mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 03:30:05 +00:00
Gets navigation bar working on mobile
This commit is contained in:
@@ -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}
|
||||||
|
</>,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 };
|
||||||
|
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 ? (
|
||||||
|
@@ -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>
|
||||||
|
@@ -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";
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user