Improve sidebar

This commit is contained in:
2025-11-04 21:51:20 +01:00
parent fdcd565431
commit 79b5a767f3
6 changed files with 68 additions and 274 deletions

View File

@@ -1,5 +1,3 @@
import { mdiMessageTextFast } from "@mdi/js";
import Icon from "@mdi/react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
@@ -59,8 +57,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
}} }}
> >
<DashboardHeader <DashboardHeader
logo={<Icon path={mdiMessageTextFast} size="2em" color={"white"} />}
title=""
menuOpen={isNavigationExpanded} menuOpen={isNavigationExpanded}
onToggleMenu={handleToggleHeaderMenu} onToggleMenu={handleToggleHeaderMenu}
/> />

View File

@@ -1,15 +1,17 @@
import * as React from "react"; import { mdiMessageTextFast } from "@mdi/js";
import { styled, useTheme } from "@mui/material/styles"; import Icon from "@mdi/react";
import Box from "@mui/material/Box"; import MenuIcon from "@mui/icons-material/Menu";
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
import MuiAppBar from "@mui/material/AppBar"; import MuiAppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import { styled } from "@mui/material/styles";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import MenuIcon from "@mui/icons-material/Menu"; import * as React from "react";
import MenuOpenIcon from "@mui/icons-material/MenuOpen"; import { RouterLink } from "../RouterLink";
import Stack from "@mui/material/Stack";
import { Link } from "react-router";
import ThemeSwitcher from "./ThemeSwitcher"; import ThemeSwitcher from "./ThemeSwitcher";
const AppBar = styled(MuiAppBar)(({ theme }) => ({ const AppBar = styled(MuiAppBar)(({ theme }) => ({
@@ -32,20 +34,14 @@ const LogoContainer = styled("div")({
}); });
export interface DashboardHeaderProps { export interface DashboardHeaderProps {
logo?: React.ReactNode;
title?: string;
menuOpen: boolean; menuOpen: boolean;
onToggleMenu: (open: boolean) => void; onToggleMenu: (open: boolean) => void;
} }
export default function DashboardHeader({ export default function DashboardHeader({
logo,
title,
menuOpen, menuOpen,
onToggleMenu, onToggleMenu,
}: DashboardHeaderProps) { }: DashboardHeaderProps) {
const theme = useTheme();
const handleMenuOpen = React.useCallback(() => { const handleMenuOpen = React.useCallback(() => {
onToggleMenu(!menuOpen); onToggleMenu(!menuOpen);
}, [menuOpen, onToggleMenu]); }, [menuOpen, onToggleMenu]);
@@ -60,7 +56,7 @@ export default function DashboardHeader({
title={`${ title={`${
isExpanded ? collapseMenuActionText : expandMenuActionText isExpanded ? collapseMenuActionText : expandMenuActionText
} menu`} } menu`}
enterDelay={1000} enterDelay={200}
> >
<div> <div>
<IconButton <IconButton
@@ -92,26 +88,25 @@ export default function DashboardHeader({
}} }}
> >
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<Box sx={{ mr: 1 }}>{getMenuIcon(menuOpen)}</Box> <Box sx={{ mr: 3 }}>{getMenuIcon(menuOpen)}</Box>
<Link to="/" style={{ textDecoration: "none" }}> <RouterLink to="/">
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
{logo ? <LogoContainer>{logo}</LogoContainer> : null} <LogoContainer>
{title ? ( <Icon path={mdiMessageTextFast} size="2em" />
<Typography </LogoContainer>
variant="h6" <Typography
sx={{ variant="h6"
color: (theme.vars ?? theme).palette.primary.main, sx={{
fontWeight: "700", fontWeight: "700",
ml: 1, ml: 1,
whiteSpace: "nowrap", whiteSpace: "nowrap",
lineHeight: 1, lineHeight: 1,
}} }}
> >
{title} MatrixGW
</Typography> </Typography>
) : null}
</Stack> </Stack>
</Link> </RouterLink>
</Stack> </Stack>
<Stack <Stack
direction="row" direction="row"

View File

@@ -1,21 +1,19 @@
import * as React from "react"; import BarChartIcon from "@mui/icons-material/BarChart";
import { useTheme } from "@mui/material/styles"; import LayersIcon from "@mui/icons-material/Layers";
import useMediaQuery from "@mui/material/useMediaQuery"; import PersonIcon from "@mui/icons-material/Person";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer"; import Drawer from "@mui/material/Drawer";
import List from "@mui/material/List"; import List from "@mui/material/List";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import { useTheme } from "@mui/material/styles";
import type {} from "@mui/material/themeCssVarsAugmentation"; import type {} from "@mui/material/themeCssVarsAugmentation";
import PersonIcon from "@mui/icons-material/Person"; import useMediaQuery from "@mui/material/useMediaQuery";
import BarChartIcon from "@mui/icons-material/BarChart"; import * as React from "react";
import DescriptionIcon from "@mui/icons-material/Description"; import { useLocation } from "react-router";
import LayersIcon from "@mui/icons-material/Layers";
import { matchPath, useLocation } from "react-router";
import DashboardSidebarContext from "./DashboardSidebarContext"; import DashboardSidebarContext from "./DashboardSidebarContext";
import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants";
import DashboardSidebarPageItem from "./DashboardSidebarPageItem";
import DashboardSidebarHeaderItem from "./DashboardSidebarHeaderItem";
import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem"; import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem";
import DashboardSidebarPageItem from "./DashboardSidebarPageItem";
import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants";
import { import {
getDrawerSxTransitionMixin, getDrawerSxTransitionMixin,
getDrawerWidthTransitionMixin, getDrawerWidthTransitionMixin,
@@ -36,10 +34,6 @@ export default function DashboardSidebar({
}: DashboardSidebarProps) { }: DashboardSidebarProps) {
const theme = useTheme(); const theme = useTheme();
const { pathname } = useLocation();
const [expandedItemIds, setExpandedItemIds] = React.useState<string[]>([]);
const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm"));
const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md"));
@@ -83,22 +77,11 @@ export default function DashboardSidebar({
[setExpanded] [setExpanded]
); );
const handlePageItemClick = React.useCallback( const handlePageItemClick = React.useCallback(() => {
(itemId: string, hasNestedNavigation: boolean) => { if (!isOverSmViewport) {
if (hasNestedNavigation && !mini) { setExpanded(false);
setExpandedItemIds((previousValue) => }
previousValue.includes(itemId) }, [mini, setExpanded, isOverSmViewport]);
? previousValue.filter(
(previousValueItemId) => previousValueItemId !== itemId
)
: [...previousValue, itemId]
);
} else if (!isOverSmViewport && !hasNestedNavigation) {
setExpanded(false);
}
},
[mini, setExpanded, isOverSmViewport]
);
const hasDrawerTransitions = const hasDrawerTransitions =
isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport);
@@ -132,67 +115,30 @@ export default function DashboardSidebar({
width: mini ? MINI_DRAWER_WIDTH : "auto", width: mini ? MINI_DRAWER_WIDTH : "auto",
}} }}
> >
<DashboardSidebarHeaderItem>Main items</DashboardSidebarHeaderItem>
<DashboardSidebarPageItem <DashboardSidebarPageItem
id="employees" id="employees"
title="Employees" title="Employees"
icon={<PersonIcon />} icon={<PersonIcon />}
href="/employees" href="/employees"
selected={
!!matchPath("/employees/*", pathname) || pathname === "/"
}
/> />
<DashboardSidebarDividerItem /> <DashboardSidebarDividerItem />
<DashboardSidebarHeaderItem>
Example items
</DashboardSidebarHeaderItem>
<DashboardSidebarPageItem <DashboardSidebarPageItem
id="reports" id="reports"
title="Reports" title="Reports"
icon={<BarChartIcon />} icon={<BarChartIcon />}
href="/reports" href="/reports"
selected={!!matchPath("/reports", pathname)}
defaultExpanded={!!matchPath("/reports", pathname)}
expanded={expandedItemIds.includes("reports")}
nestedNavigation={
<List
dense
sx={{
padding: 0,
my: 1,
pl: mini ? 0 : 1,
minWidth: 240,
}}
>
<DashboardSidebarPageItem
id="sales"
title="Sales"
icon={<DescriptionIcon />}
href="/reports/sales"
selected={!!matchPath("/reports/sales", pathname)}
/>
<DashboardSidebarPageItem
id="traffic"
title="Traffic"
icon={<DescriptionIcon />}
href="/reports/traffic"
selected={!!matchPath("/reports/traffic", pathname)}
/>
</List>
}
/> />
<DashboardSidebarPageItem <DashboardSidebarPageItem
id="integrations" id="integrations"
title="Integrations" title="Integrations"
icon={<LayersIcon />} icon={<LayersIcon />}
href="/integrations" href="/integrations"
selected={!!matchPath("/integrations", pathname)}
/> />
</List> </List>
</Box> </Box>
</React.Fragment> </React.Fragment>
), ),
[mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname] [mini, hasDrawerTransitions, isFullyExpanded]
); );
const getDrawerSharedSx = React.useCallback( const getDrawerSharedSx = React.useCallback(

View File

@@ -1,5 +1,11 @@
import * as React from "react"; import * as React from "react";
const DashboardSidebarContext = React.createContext(null); const DashboardSidebarContext = React.createContext<{
onPageItemClick: () => void;
mini: boolean;
fullyExpanded: boolean;
fullyCollapsed: boolean;
hasDrawerTransitions: boolean;
} | null>(null);
export default DashboardSidebarContext; export default DashboardSidebarContext;

View File

@@ -1,46 +0,0 @@
import * as React from "react";
import ListSubheader from "@mui/material/ListSubheader";
import type {} from "@mui/material/themeCssVarsAugmentation";
import DashboardSidebarContext from "./DashboardSidebarContext";
import { DRAWER_WIDTH } from "./constants";
import { getDrawerSxTransitionMixin } from "./mixins";
export interface DashboardSidebarHeaderItemProps {
children?: React.ReactNode;
}
export default function DashboardSidebarHeaderItem({
children,
}: DashboardSidebarHeaderItemProps) {
const sidebarContext = React.useContext(DashboardSidebarContext);
if (!sidebarContext) {
throw new Error("Sidebar context was used without a provider.");
}
const {
mini = false,
fullyExpanded = true,
hasDrawerTransitions,
} = sidebarContext;
return (
<ListSubheader
sx={{
fontSize: 12,
fontWeight: "600",
height: mini ? 0 : 36,
...(hasDrawerTransitions
? getDrawerSxTransitionMixin(fullyExpanded, "height")
: {}),
px: 1.5,
py: 0,
minWidth: DRAWER_WIDTH,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
zIndex: 2,
}}
>
{children}
</ListSubheader>
);
}

View File

@@ -1,18 +1,13 @@
import * as React from "react";
import { type Theme, type SxProps } from "@mui/material/styles";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Collapse from "@mui/material/Collapse";
import Grow from "@mui/material/Grow";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import type {} from "@mui/material/themeCssVarsAugmentation"; import type {} from "@mui/material/themeCssVarsAugmentation";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import * as React from "react";
import { Link } from "react-router"; import { Link, matchPath, useLocation } from "react-router";
import DashboardSidebarContext from "./DashboardSidebarContext"; import DashboardSidebarContext from "./DashboardSidebarContext";
import { MINI_DRAWER_WIDTH } from "./constants"; import { MINI_DRAWER_WIDTH } from "./constants";
@@ -22,11 +17,7 @@ export interface DashboardSidebarPageItemProps {
icon?: React.ReactNode; icon?: React.ReactNode;
href: string; href: string;
action?: React.ReactNode; action?: React.ReactNode;
defaultExpanded?: boolean;
expanded?: boolean;
selected?: boolean;
disabled?: boolean; disabled?: boolean;
nestedNavigation?: React.ReactNode;
} }
export default function DashboardSidebarPageItem({ export default function DashboardSidebarPageItem({
@@ -35,12 +26,10 @@ export default function DashboardSidebarPageItem({
icon, icon,
href, href,
action, action,
defaultExpanded = false,
expanded = defaultExpanded,
selected = false,
disabled = false, disabled = false,
nestedNavigation,
}: DashboardSidebarPageItemProps) { }: DashboardSidebarPageItemProps) {
const { pathname } = useLocation();
const sidebarContext = React.useContext(DashboardSidebarContext); const sidebarContext = React.useContext(DashboardSidebarContext);
if (!sidebarContext) { if (!sidebarContext) {
throw new Error("Sidebar context was used without a provider."); throw new Error("Sidebar context was used without a provider.");
@@ -49,38 +38,13 @@ export default function DashboardSidebarPageItem({
onPageItemClick, onPageItemClick,
mini = false, mini = false,
fullyExpanded = true, fullyExpanded = true,
fullyCollapsed = false,
} = sidebarContext; } = sidebarContext;
const [isHovered, setIsHovered] = React.useState(false);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (onPageItemClick) { if (onPageItemClick) {
onPageItemClick(id, !!nestedNavigation); onPageItemClick();
} }
}, [onPageItemClick, id, nestedNavigation]); }, [onPageItemClick, id]);
let nestedNavigationCollapseSx: SxProps<Theme> = { display: "none" };
if (mini && fullyCollapsed) {
nestedNavigationCollapseSx = {
fontSize: 18,
position: "absolute",
top: "41.5%",
right: "2px",
transform: "translateY(-50%) rotate(-90deg)",
};
} else if (!mini && fullyExpanded) {
nestedNavigationCollapseSx = {
ml: 0.5,
fontSize: 20,
transform: `rotate(${expanded ? 0 : -90}deg)`,
transition: (theme: Theme) =>
theme.transitions.create("transform", {
easing: theme.transitions.easing.sharp,
duration: 100,
}),
};
}
const hasExternalHref = href const hasExternalHref = href
? href.startsWith("http://") || href.startsWith("https://") ? href.startsWith("http://") || href.startsWith("https://")
@@ -88,61 +52,28 @@ export default function DashboardSidebarPageItem({
const LinkComponent = hasExternalHref ? "a" : Link; const LinkComponent = hasExternalHref ? "a" : Link;
const miniNestedNavigationSidebarContextValue = React.useMemo(() => { const selected = !!matchPath(href, pathname);
return {
onPageItemClick: onPageItemClick ?? (() => {}),
mini: false,
fullyExpanded: true,
fullyCollapsed: false,
hasDrawerTransitions: false,
};
}, [onPageItemClick]);
return ( return (
<React.Fragment> <React.Fragment>
<ListItem <ListItem disablePadding>
disablePadding
{...(nestedNavigation && mini
? {
onMouseEnter: () => {
setIsHovered(true);
},
onMouseLeave: () => {
setIsHovered(false);
},
}
: {})}
sx={{
display: "block",
py: 0,
px: 1,
overflowX: "hidden",
}}
>
<ListItemButton <ListItemButton
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
sx={{ sx={{
height: mini ? 50 : "auto", height: mini ? 50 : "auto",
}} }}
{...(nestedNavigation && !mini {...{
? { LinkComponent,
onClick: handleClick, ...(hasExternalHref
} ? {
: {})} target: "_blank",
{...(!nestedNavigation rel: "noopener noreferrer",
? { }
LinkComponent, : {}),
...(hasExternalHref to: href,
? { onClick: handleClick,
target: "_blank", }}
rel: "noopener noreferrer",
}
: {}),
to: href,
onClick: handleClick,
}
: {})}
> >
{icon || mini ? ( {icon || mini ? (
<Box <Box
@@ -212,42 +143,8 @@ export default function DashboardSidebarPageItem({
/> />
) : null} ) : null}
{action && !mini && fullyExpanded ? action : null} {action && !mini && fullyExpanded ? action : null}
{nestedNavigation ? (
<ExpandMoreIcon sx={nestedNavigationCollapseSx} />
) : null}
</ListItemButton> </ListItemButton>
{nestedNavigation && mini ? (
<Grow in={isHovered}>
<Box
sx={{
position: "fixed",
left: MINI_DRAWER_WIDTH - 2,
pl: "6px",
}}
>
<Paper
elevation={8}
sx={{
pt: 0.2,
pb: 0.2,
transform: "translateY(-50px)",
}}
>
<DashboardSidebarContext.Provider
value={miniNestedNavigationSidebarContextValue}
>
{nestedNavigation}
</DashboardSidebarContext.Provider>
</Paper>
</Box>
</Grow>
) : null}
</ListItem> </ListItem>
{nestedNavigation && !mini ? (
<Collapse in={expanded} timeout="auto" unmountOnExit>
{nestedNavigation}
</Collapse>
) : null}
</React.Fragment> </React.Fragment>
); );
} }