Gets navigation bar working on mobile

This commit is contained in:
Julian Reinhardt
2021-10-26 23:39:36 +02:00
parent 554002854c
commit dea89bd312
7 changed files with 184 additions and 142 deletions

View File

@@ -1,6 +1,7 @@
import React, { ReactNode } from "react";
import cn from "classnames";
import { Link, LinkProps } from "react-router-dom";
export interface DropdownItemProps {
/**
@@ -28,9 +29,9 @@ export interface DropdownItemProps {
*/
icon?: ReactNode;
/**
* Href
* Optional react-router-dom `to` prop, will convert the item to a link
*/
href?: string;
to?: string;
/**
* onClick handler
*/
@@ -43,25 +44,37 @@ export const DropdownItem: React.FC<DropdownItemProps> = ({
active,
disabled,
icon,
href,
to,
onClick,
...rest
}) => {
const getElem = (props: Omit<LinkProps, "to">, children: ReactNode) => {
return to ? (
<Link to={to} {...props}>
{children}
</Link>
) : (
<span {...props}> {children} </span>
);
};
return divider ? (
<div className={cn("dropdown-divider", className)} />
) : (
<a
className={cn(
"dropdown-item",
active && "active",
disabled && "disabled",
className,
)}
href={href}
onClick={onClick}
{...rest}>
{icon && <span className="dropdown-item-icon">{icon}</span>}
{children}
</a>
getElem(
{
className: cn(
"dropdown-item",
active && "active",
disabled && "disabled",
className,
),
onClick,
...rest,
},
<>
{icon && <span className="dropdown-item-icon">{icon}</span>}
{children}
</>,
)
);
};

View File

@@ -2,7 +2,6 @@ import React from "react";
import { Dropdown, Navigation } from "components";
import { intl } from "locale";
import { Link } from "react-router-dom";
import {
Book,
DeviceDesktop,
@@ -13,11 +12,13 @@ import {
Users,
} from "tabler-icons-react";
function NavMenu() {
const NavMenu: React.FC<{ openOnMobile: boolean }> = ({ openOnMobile }) => {
return (
<Navigation.Menu
theme="light"
className="mb-3"
withinHeader={true}
openOnMobile={openOnMobile}
items={[
{
title: intl.formatMessage({
@@ -47,25 +48,27 @@ function NavMenu() {
title: "SSL",
icon: <Shield />,
dropdownItems: [
<Dropdown.Item key="ssl-certificates">
<Link to="/ssl/certificates" role="button" aria-expanded="false">
<span className="nav-link-title">
{intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
})}
</span>
</Link>
<Dropdown.Item
key="ssl-certificates"
to="/ssl/certificates"
role="button">
<span className="nav-link-title">
{intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
})}
</span>
</Dropdown.Item>,
<Dropdown.Item key="ssl-authorities">
<Link to="/ssl/authorities" role="button" aria-expanded="false">
<span className="nav-link-title">
{intl.formatMessage({
id: "cert_authorities.title",
defaultMessage: "Certificate Authorities",
})}
</span>
</Link>
<Dropdown.Item
key="ssl-authorities"
to="/ssl/authorities"
role="button">
<span className="nav-link-title">
{intl.formatMessage({
id: "cert_authorities.title",
defaultMessage: "Certificate Authorities",
})}
</span>
</Dropdown.Item>,
],
},
@@ -96,6 +99,6 @@ function NavMenu() {
]}
/>
);
}
};
export { NavMenu };

View File

@@ -3,12 +3,12 @@ import React, { ReactNode, useState, useRef, useEffect } from "react";
import cn from "classnames";
import { Bell } from "tabler-icons-react";
import { NavMenu } from "..";
import { Badge } from "../Badge";
import { ButtonList } from "../ButtonList";
import { Dropdown } from "../Dropdown";
import { NavigationMenu } from "./NavigationMenu";
import { NavigationMenuItemProps } from "./NavigationMenuItem";
export interface NavigationHeaderProps {
/**
* Additional Class
@@ -75,9 +75,15 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
}) => {
const [notificationsShown, setNotificationsShown] = useState(false);
const [profileShown, setProfileShown] = useState(false);
const [mobileNavShown, setMobileNavShown] = useState(false);
const profileRef = useRef(null);
const notificationsRef = useRef(null);
const toggleMobileNavShown = () =>
setMobileNavShown((prevState) => {
return !prevState;
});
const handleClickOutside = (event: any) => {
if (
profileRef.current &&
@@ -101,97 +107,105 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
}, []);
return (
<header
className={cn(
`navbar navbar-expand-md navbar-${theme} d-print-none`,
className,
)}>
<div className="container-xl">
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar-menu">
<span className="navbar-toggler-icon" />
</button>
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
{brandContent}
</h1>
<div className="navbar-nav flex-row order-md-last">
{buttons ? (
<div className="nav-item d-none d-md-flex me-3">
<ButtonList>{buttons}</ButtonList>
</div>
) : null}
{notifications ? (
<>
<header
className={cn(
`navbar navbar-expand-md navbar-${theme} d-print-none`,
className,
)}>
<div className="container-xl">
<button
className="navbar-toggler"
type="button"
onClick={toggleMobileNavShown}>
<span className="navbar-toggler-icon" />
</button>
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
{brandContent}
</h1>
<div className="navbar-nav flex-row order-md-last">
{buttons ? (
<div className="nav-item d-none d-md-flex me-3">
<ButtonList>{buttons}</ButtonList>
</div>
) : null}
{notifications ? (
<div
className="nav-item dropdown d-none d-md-flex me-3"
ref={notificationsRef}>
<button
style={{
border: 0,
backgroundColor: "transparent",
}}
className="nav-link px-0"
aria-label="Show notifications"
onClick={() => {
setNotificationsShown(!notificationsShown);
}}>
<Bell className="icon" />
{hasUnreadNotifications ? <Badge color="red" /> : null}
</button>
<Dropdown
className="dropdown-menu-end dropdown-menu-card"
show={notificationsShown}
dark={darkDropdowns}>
<div className="card">
<div className="card-body">{notifications}</div>
</div>
</Dropdown>
</div>
) : null}
<div
className="nav-item dropdown d-none d-md-flex me-3"
ref={notificationsRef}>
ref={profileRef}
className={cn("nav-item", {
dropdown: !!profileItems,
})}>
<button
style={{
border: 0,
backgroundColor: "transparent",
}}
className="nav-link px-0"
aria-label="Show notifications"
className="nav-link d-flex lh-1 text-reset p-0"
aria-label={profileItems && "Open user menu"}
onClick={() => {
setNotificationsShown(!notificationsShown);
setProfileShown(!profileShown);
}}>
<Bell className="icon" />
{hasUnreadNotifications ? <Badge color="red" /> : null}
{avatar}
{profileName ? (
<div className="d-none d-xl-block ps-2">
<div style={{ textAlign: "left" }}>{profileName}</div>
{profileSubName ? (
<div
className="mt-1 small text-muted"
style={{ textAlign: "left" }}>
{profileSubName}
</div>
) : null}
</div>
) : null}
</button>
<Dropdown
className="dropdown-menu-end dropdown-menu-card"
show={notificationsShown}
dark={darkDropdowns}>
<div className="card">
<div className="card-body">{notifications}</div>
</div>
</Dropdown>
</div>
) : null}
<div
ref={profileRef}
className={cn("nav-item", {
dropdown: !!profileItems,
})}>
<button
style={{
border: 0,
backgroundColor: "transparent",
}}
className="nav-link d-flex lh-1 text-reset p-0"
aria-label={profileItems && "Open user menu"}
onClick={() => {
setProfileShown(!profileShown);
}}>
{avatar}
{profileName ? (
<div className="d-none d-xl-block ps-2">
<div style={{ textAlign: "left" }}>{profileName}</div>
{profileSubName ? (
<div
className="mt-1 small text-muted"
style={{ textAlign: "left" }}>
{profileSubName}
</div>
) : null}
</div>
{profileItems ? (
<Dropdown
className="dropdown-menu-end dropdown-menu-card"
show={profileShown}
dark={darkDropdowns}
arrow>
{profileItems}
</Dropdown>
) : null}
</button>
{profileItems ? (
<Dropdown
className="dropdown-menu-end dropdown-menu-card"
show={profileShown}
dark={darkDropdowns}
arrow>
{profileItems}
</Dropdown>
) : null}
</div>
</div>
{menuItems ? (
<NavigationMenu
items={menuItems}
withinHeader
openOnMobile={false}
/>
) : null}
</div>
{menuItems ? <NavigationMenu items={menuItems} withinHeader /> : null}
</div>
</header>
</header>
<NavMenu openOnMobile={mobileNavShown} />
</>
);
};

View File

@@ -1,6 +1,7 @@
import React, { ReactNode, useState, useRef, useEffect } from "react";
import cn from "classnames";
import styled from "styled-components";
import {
NavigationMenuItem,
@@ -15,27 +16,29 @@ import {
* the items.
*/
const StyledNavWrapper = styled.div<{ shown: boolean }>`
@media (max-width: 767.98px) {
transition: max-height 300ms ease-in-out;
max-height: ${(p) => (p.shown ? `80vh` : `0`)};
min-height: ${(p) => (p.shown ? `inherit` : `0`)};
overflow: hidden;
padding: ${(p) => (p.shown ? `inherit` : `0`)};
}
`;
export interface NavigationMenuProps {
/**
* Additional Class
*/
/** Additional Class */
className?: string;
/**
* Navigation Items
*/
/** Navigation Items */
items: NavigationMenuItemProps[];
/**
* If this menu sits within a Navigation.Header
*/
/** If this menu sits within a Navigation.Header */
withinHeader?: boolean;
/**
* Color theme for the nav bar
*/
/** Color theme for the nav bar */
theme?: "transparent" | "light" | "dark";
/**
* Search content
*/
/** Search content */
searchContent?: ReactNode;
/** Navigation is currently hidden on mobile */
openOnMobile?: boolean;
}
export const NavigationMenu: React.FC<NavigationMenuProps> = ({
className,
@@ -43,6 +46,7 @@ export const NavigationMenu: React.FC<NavigationMenuProps> = ({
withinHeader,
theme = "transparent",
searchContent,
openOnMobile = true,
}) => {
const [dropdownShown, setDropdownShown] = useState(0);
const navRef = useRef(null);
@@ -74,16 +78,20 @@ export const NavigationMenu: React.FC<NavigationMenuProps> = ({
const wrapMenu = (el: ReactNode) => {
if (withinHeader) {
return (
<div className={cn("collapse navbar-collapse", className)}>
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
{el}
</div>
<div className="navbar-expand-md">
<StyledNavWrapper
shown={openOnMobile}
className={cn(`navbar navbar-${theme} navbar-collapse`, className)}>
<div className="container-xl">{el}</div>
</StyledNavWrapper>
</div>
);
}
return (
<div className={"navbar-expand-md"}>
<div className={cn(`navbar navbar-${theme}`, className)}>
<div
className={cn(`navbar navbar-${theme}`, className)}
id="navbar-menu">
<div className="container-xl">
{el}
{searchContent ? (

View File

@@ -7,8 +7,6 @@ import { useAuthState, useUserState } from "context";
import { intl } from "locale";
import styled from "styled-components";
import { NavMenu } from "./NavMenu";
const StyledSiteContainer = styled.div`
display: flex;
flex-direction: column;
@@ -77,7 +75,6 @@ function SiteWrapper({ children }: Props) {
</Dropdown.Item>,
]}
/>
<NavMenu />
<div className="content">
<div className="container-xl">
<StyledContentContainer>{children}</StyledContentContainer>

View File

@@ -1,4 +1,4 @@
import React, { ReactNode } from "react";
import React from "react";
import cn from "classnames";
import { Badge } from "components";

View File

@@ -18,6 +18,13 @@ body {
text-align: right;
}
// Hide the dropdown arrow when it is inlined in the mobile menu
@media (max-width: 767.98px) {
.navbar-collapse .dropdown-menu-arrow::before {
display: none;
}
}
/*
.btn {
text-transform: none;