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 React, { ReactNode } from "react";
import cn from "classnames"; import cn from "classnames";
import { Link, LinkProps } from "react-router-dom";
export interface DropdownItemProps { export interface DropdownItemProps {
/** /**
@@ -28,9 +29,9 @@ export interface DropdownItemProps {
*/ */
icon?: ReactNode; icon?: ReactNode;
/** /**
* Href * Optional react-router-dom `to` prop, will convert the item to a link
*/ */
href?: string; to?: string;
/** /**
* onClick handler * onClick handler
*/ */
@@ -43,25 +44,37 @@ export const DropdownItem: React.FC<DropdownItemProps> = ({
active, active,
disabled, disabled,
icon, icon,
href, to,
onClick, onClick,
...rest ...rest
}) => { }) => {
const getElem = (props: Omit<LinkProps, "to">, children: ReactNode) => {
return to ? (
<Link to={to} {...props}>
{children}
</Link>
) : (
<span {...props}> {children} </span>
);
};
return divider ? ( return divider ? (
<div className={cn("dropdown-divider", className)} /> <div className={cn("dropdown-divider", className)} />
) : ( ) : (
<a getElem(
className={cn( {
"dropdown-item", className: cn(
active && "active", "dropdown-item",
disabled && "disabled", active && "active",
className, disabled && "disabled",
)} className,
href={href} ),
onClick={onClick} onClick,
{...rest}> ...rest,
{icon && <span className="dropdown-item-icon">{icon}</span>} },
{children} <>
</a> {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 { Dropdown, Navigation } from "components";
import { intl } from "locale"; import { intl } from "locale";
import { Link } from "react-router-dom";
import { import {
Book, Book,
DeviceDesktop, DeviceDesktop,
@@ -13,11 +12,13 @@ import {
Users, Users,
} from "tabler-icons-react"; } from "tabler-icons-react";
function NavMenu() { const NavMenu: React.FC<{ openOnMobile: boolean }> = ({ openOnMobile }) => {
return ( return (
<Navigation.Menu <Navigation.Menu
theme="light" theme="light"
className="mb-3" className="mb-3"
withinHeader={true}
openOnMobile={openOnMobile}
items={[ items={[
{ {
title: intl.formatMessage({ title: intl.formatMessage({
@@ -47,25 +48,27 @@ function NavMenu() {
title: "SSL", title: "SSL",
icon: <Shield />, icon: <Shield />,
dropdownItems: [ dropdownItems: [
<Dropdown.Item key="ssl-certificates"> <Dropdown.Item
<Link to="/ssl/certificates" role="button" aria-expanded="false"> key="ssl-certificates"
<span className="nav-link-title"> to="/ssl/certificates"
{intl.formatMessage({ role="button">
id: "certificates.title", <span className="nav-link-title">
defaultMessage: "Certificates", {intl.formatMessage({
})} id: "certificates.title",
</span> defaultMessage: "Certificates",
</Link> })}
</span>
</Dropdown.Item>, </Dropdown.Item>,
<Dropdown.Item key="ssl-authorities"> <Dropdown.Item
<Link to="/ssl/authorities" role="button" aria-expanded="false"> key="ssl-authorities"
<span className="nav-link-title"> to="/ssl/authorities"
{intl.formatMessage({ role="button">
id: "cert_authorities.title", <span className="nav-link-title">
defaultMessage: "Certificate Authorities", {intl.formatMessage({
})} id: "cert_authorities.title",
</span> defaultMessage: "Certificate Authorities",
</Link> })}
</span>
</Dropdown.Item>, </Dropdown.Item>,
], ],
}, },
@@ -96,6 +99,6 @@ function NavMenu() {
]} ]}
/> />
); );
} };
export { NavMenu }; export { NavMenu };

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,13 @@ body {
text-align: right; 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 { .btn {
text-transform: none; text-transform: none;