diff --git a/src/components/navigation/SidebarNav.stories.tsx b/src/components/navigation/SidebarNav.stories.tsx index e72c6b52..c96fb962 100644 --- a/src/components/navigation/SidebarNav.stories.tsx +++ b/src/components/navigation/SidebarNav.stories.tsx @@ -18,6 +18,13 @@ import { Toolbar, Typography, } from "../MUI/MuiWrapped"; +import { + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, +} from "@mui/material"; import { Theme } from "@mui/material/styles"; import { Logo } from "../controls/Logo"; import { ColourSchemeButton } from "../controls/ColourSchemeButton"; @@ -45,6 +52,9 @@ A collapsing/expanding sidebar for your app's primary navigation. For normal screen sizes, the implementation uses MUI's permanent drawer toggling between two widths showing either icon and text or just icon. For smaller screens, we use the temporary variant instead.`, }, + story: { + height: "600px", + }, }, }, }; @@ -76,24 +86,25 @@ const standardLinks = [ ]; export const NormalLinks: Story = { - render: (args) => { + render: (_args) => { + const [open, setOpen] = React.useState(true); return ( - - - When using standard links, the caller must handle the selected state - and set it to the correct item. - + + + setOpen(!open)}> + + + + When using standard links, the caller must handle the selected state + and set it to the correct item. + + ); }, args: { navigation: standardLinks, - open: true, }, }; @@ -129,23 +140,29 @@ const reactRouterNavigation = [ ]; export const RouterLinks: Story = { - render: (args) => { + render: (_args) => { + const [open, setOpen] = React.useState(false); return ( - - React Router NavLinks will handle selected state internally. - + + setOpen(!open)}> + + + + React Router NavLinks will handle selected state + internally. + + ); }, args: { navigation: reactRouterNavigation, - open: false, }, }; @@ -190,21 +207,120 @@ const groupedNavigation = [ ]; export const GroupedNavigation: Story = { - render: (args) => { + render: (_args) => { + const [open, setOpen] = React.useState(true); return ( - Sections are grouped with dividers + + setOpen(!open)}> + + + Sections are grouped with dividers. + ); }, - args: { - navigation: groupedNavigation, - open: true, +}; + +/** A dashed, tinted wrapper so it's obvious in the story which content is coming from a slot vs. the `navigation` prop. */ +const SlotOutline = ({ + label, + open, + children, +}: { + label: string; + open: boolean; + children: React.ReactNode; +}) => ( + + {open && ( + + {label} + + )} + {children} + +); + +export const WithSlots: Story = { + render: (_args) => { + const [open, setOpen] = React.useState(true); + return ( + + + + + + + + + + + + } + footerSlot={ + + + + + + + + + + + } + /> + + setOpen(!open)}> + + + + Adds slots to the navbar, boxes are only there to highlight what + each slot renders, they aren't part of the component.{" "} + afterNavSlot renders inside the scrollable area, right + after the navigation items. footerSlot is pinned to the + bottom of the drawer, outside the scroll area. + + + + ); }, }; diff --git a/src/components/navigation/SidebarNav.test.tsx b/src/components/navigation/SidebarNav.test.tsx index cb3a6518..a282b333 100644 --- a/src/components/navigation/SidebarNav.test.tsx +++ b/src/components/navigation/SidebarNav.test.tsx @@ -121,6 +121,44 @@ describe("SidebarNav", () => { expect(divider).toBeInTheDocument(); }); + it("renders afterNavSlot after the navigation items", () => { + const router = createMemoryRouter([ + { + path: "/", + element: ( + Extra links} + /> + ), + }, + ]); + render(); + + expect(screen.getByTestId("after-nav")).toBeVisible(); + }); + + it("renders footerSlot", () => { + const router = createMemoryRouter([ + { + path: "/", + element: ( + User menu} + /> + ), + }, + ]); + render(); + + expect(screen.getByTestId("footer")).toBeVisible(); + }); + it("renders internal and external links with correct href", () => { // even though specified differently, ultimately both types // should have the correct href attribute diff --git a/src/components/navigation/SidebarNav.tsx b/src/components/navigation/SidebarNav.tsx index 327f7106..3a62e51b 100644 --- a/src/components/navigation/SidebarNav.tsx +++ b/src/components/navigation/SidebarNav.tsx @@ -59,6 +59,10 @@ type NavProps = { navigation: Navigation; open: boolean; setOpen: (open: boolean) => void; + /** Rendered after the navigation items, inside the scrollable area. */ + afterNavSlot?: ReactNode; + /** Rendered pinned to the bottom of the drawer, outside the scrollable area. */ + footerSlot?: ReactNode; }; export function SidebarNav(props: NavProps) { @@ -87,13 +91,14 @@ function PermanentDrawer(props: NavProps) { transition: (theme: Theme) => drawerTransition(theme, props.open), [`& .MuiDrawer-paper`]: { width: width, + height: "100vh", boxSizing: "border-box", transition: drawerTransition(theme, props.open), }, })} > {/* spacer equal to the AppBar's height*/} - + ); } @@ -124,14 +129,35 @@ function TemporaryDrawer(props: NavProps) { }} > - + ); } -function NavigationItems({ navigation, open }: NavProps) { +function DrawerContent(props: NavProps) { + return ( + + + {props.footerSlot && ( + + + {props.footerSlot} + + )} + + ); +} + +function NavigationItems({ navigation, open, afterNavSlot }: NavProps) { return ( - + ))} + {afterNavSlot} ); }