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(
|
{
|
||||||
|
className: cn(
|
||||||
"dropdown-item",
|
"dropdown-item",
|
||||||
active && "active",
|
active && "active",
|
||||||
disabled && "disabled",
|
disabled && "disabled",
|
||||||
className,
|
className,
|
||||||
)}
|
),
|
||||||
href={href}
|
onClick,
|
||||||
onClick={onClick}
|
...rest,
|
||||||
{...rest}>
|
},
|
||||||
|
<>
|
||||||
{icon && <span className="dropdown-item-icon">{icon}</span>}
|
{icon && <span className="dropdown-item-icon">{icon}</span>}
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</>,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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"
|
||||||
|
to="/ssl/certificates"
|
||||||
|
role="button">
|
||||||
<span className="nav-link-title">
|
<span className="nav-link-title">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "certificates.title",
|
id: "certificates.title",
|
||||||
defaultMessage: "Certificates",
|
defaultMessage: "Certificates",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
<Dropdown.Item key="ssl-authorities">
|
<Dropdown.Item
|
||||||
<Link to="/ssl/authorities" role="button" aria-expanded="false">
|
key="ssl-authorities"
|
||||||
|
to="/ssl/authorities"
|
||||||
|
role="button">
|
||||||
<span className="nav-link-title">
|
<span className="nav-link-title">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "cert_authorities.title",
|
id: "cert_authorities.title",
|
||||||
defaultMessage: "Certificate Authorities",
|
defaultMessage: "Certificate Authorities",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
|
||||||
</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,6 +107,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
`navbar navbar-expand-md navbar-${theme} d-print-none`,
|
`navbar navbar-expand-md navbar-${theme} d-print-none`,
|
||||||
@@ -110,8 +117,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
|||||||
<button
|
<button
|
||||||
className="navbar-toggler"
|
className="navbar-toggler"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
onClick={toggleMobileNavShown}>
|
||||||
data-bs-target="#navbar-menu">
|
|
||||||
<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">
|
||||||
@@ -190,8 +196,16 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{menuItems ? <NavigationMenu items={menuItems} withinHeader /> : null}
|
{menuItems ? (
|
||||||
|
<NavigationMenu
|
||||||
|
items={menuItems}
|
||||||
|
withinHeader
|
||||||
|
openOnMobile={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<NavMenu openOnMobile={mobileNavShown} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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