{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "mobile-nav", "title": "Mobile Navigation Drawer", "description": "Hamburger trigger opening a left-side sheet containing the multi-level push menu — the mobile navigation pattern for NSW site headers.", "registryDependencies": [ "https://ui.digital.nsw.gov.au/registry/r/theme.json", "https://ui.digital.nsw.gov.au/registry/r/button.json", "https://ui.digital.nsw.gov.au/registry/r/sheet.json", "https://ui.digital.nsw.gov.au/registry/r/push-menu.json", "https://ui.digital.nsw.gov.au/registry/r/icons.json" ], "files": [ { "path": "src/patterns/mobile-nav.tsx", "content": "'use client'\n\nimport * as React from 'react'\n\nimport { Button } from '@/components/button'\nimport { PushMenu, type PushMenuItem } from '@/components/push-menu'\nimport { Sheet, SheetContent, SheetTitle, SheetTrigger } from '@/components/sheet'\nimport { IconMenu } from '@/icons/menu'\n\nexport type MobileNavProps = {\n /** The menu tree, passed straight to `PushMenu`. Item ids must be unique. */\n navigation: PushMenuItem[]\n /**\n * The app's current pathname. Leaf links whose `href` matches get\n * `aria-current=\"page\"` and the active treatment (see `PushMenu`).\n */\n currentHref?: string\n /**\n * Drawer title: names the dialog for assistive tech (via a visually-hidden\n * `SheetTitle`) and is shown by `PushMenu` as its root level heading.\n * Defaults to `\"Menu\"`.\n */\n title?: string\n /** Controlled open state. Leave unset for uncontrolled. */\n open?: boolean\n /** Initial open state when uncontrolled. Defaults to `false`. */\n defaultOpen?: boolean\n /**\n * Fired whenever the drawer opens or closes — trigger click, Escape,\n * backdrop click, the menu's own close button, and leaf-link clicks all\n * funnel through here. Pair with `open` for a controlled drawer.\n */\n onOpenChange?: (open: boolean) => void\n /** Extra drawer content rendered below the menu — a sign-in link, say. */\n children?: React.ReactNode\n}\n\n/**\n * Mobile navigation drawer: a hamburger button that opens a left-side sheet\n * containing the multi-level `PushMenu`. A registry block — copy the source\n * and adapt it — composed entirely from published components: `Button`\n * (trigger), `Sheet` (the drawer, on the Base UI dialog primitive) and\n * `PushMenu` (the drill-down menu).\n *\n * Accessibility contract:\n *\n * - Base UI's dialog provides the modal behaviour — focus trap, scroll lock,\n * Escape and backdrop dismissal, and focus return to the trigger on close.\n * Nothing is hand-rolled (the nswds-app source hand-assembled this from\n * Headless UI Dialog + fixed-position divs).\n * - The dialog's accessible name comes from a visually-hidden `SheetTitle`.\n * It is `sr-only` because `PushMenu` already renders a visible title in its\n * header row: a second visible \"Menu\" heading would duplicate it, but the\n * per-level heading can't name the dialog either — it changes as the user\n * drills, and it isn't wired to the popup's `aria-labelledby`. Same\n * arrangement as sidebar-style sheets elsewhere in the shadcn ecosystem.\n * - The sheet's built-in close button is disabled (`showCloseButton={false}`)\n * in favour of `PushMenu`'s own header close button — two overlapping close\n * affordances in the same corner would be one too many, and the menu's\n * button participates in its focus management. `onClose` is wired to close\n * the sheet, and leaf-link clicks close it too (`onItemClick`), so choosing\n * a destination never strands the drawer over the new page.\n * - Open state is controlled-or-uncontrolled: the sheet itself is always\n * driven from one internal source of truth so the menu's close paths work\n * in both modes, and `onOpenChange` reports every transition.\n *\n * Departures from the nswds-app source (`MobileHeader`):\n *\n * - The logo/badge/centering row and the `lg:hidden` breakpoint wrapper are\n * gone — the published `Header` already renders the brand lockup at every\n * width, so this block is only the navigation drawer. Place it inside\n * `HeaderActions` and hide it at desktop widths from the outside (e.g. a\n * `lg:hidden` wrapper) if the service swaps to a horizontal nav there.\n * - `MobileSearch` is out of scope; compose `ExpandableSearch` or your own\n * search alongside this block in `HeaderActions`.\n * - The hand-written slide transition is replaced by `SheetContent`'s, which\n * respects `prefers-reduced-motion`.\n */\nexport function MobileNav({\n navigation,\n currentHref,\n title = 'Menu',\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n children,\n}: MobileNavProps) {\n // Controlled-or-uncontrolled: the Sheet is always handed a single resolved\n // `open` so the menu's close paths (header close button, leaf-link clicks)\n // work identically in both modes. When `open` is supplied, the internal\n // state is bypassed and the consumer decides what `onOpenChange` does.\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)\n const isControlled = openProp !== undefined\n const open = isControlled ? openProp : uncontrolledOpen\n\n function setOpen(next: boolean) {\n if (!isControlled) {\n setUncontrolledOpen(next)\n }\n onOpenChange?.(next)\n }\n\n return (\n setOpen(next)}>\n {/* aria-label, not visible text: the trigger is icon-only, and Button's\n dev-only guard would flag it unnamed otherwise (WCAG 2.2, 4.1.2). */}\n \n }\n />\n {/* Width comes from SheetContent's own left-side treatment (w-3/4,\n sm:max-w-sm) — overriding it needs the data-[side=left]: prefix, or\n the default's higher-specificity variant class wins. p-0 guards\n against any future default padding: PushMenu supplies its own. */}\n \n {title}\n {/* h-auto + flex-1 override PushMenu's own h-full so extra drawer\n content (children) can share the column; min-h-0 lets the menu's\n internal scroll area shrink instead of overflowing the sheet. */}\n setOpen(false)}\n onItemClick={() => setOpen(false)}\n className='h-auto min-h-0 flex-1'\n />\n {children ? (\n
\n {children}\n
\n ) : null}\n
\n
\n )\n}\n", "type": "registry:component", "target": "components/mobile-nav.tsx" } ], "type": "registry:block", "meta": { "nswdsVersion": "5.1.0" } }