import React__default from 'react'; /** * Default slide duration for level transitions, in milliseconds — the default * of the `durationMs` prop. * * COUPLING: `durationMs` drives BOTH the `--push-menu-duration` custom * property (set inline on the root, read by the CSS transition) and the state * machine's `setTimeout`s (which settle the animation state, pop levels, and * schedule focus restoration). Do not override the custom property directly — * that changes only the visual duration while the state machine still settles * on `durationMs`, desyncing the two; pass `durationMs` instead. Under * `prefers-reduced-motion` the slide is skipped (the transition is applied * under `motion-safe:` only) but the state machine still waits the full * duration; the menu is simply "settled early" for those users, never broken. */ declare const PUSH_MENU_DURATION_MS = 300; /** A node in the menu tree. Items with `links` drill in; items with `href` navigate. */ type PushMenuItem = { /** Unique across the whole tree — level ids and focus restoration track items by id. */ id: string; /** Visible label. */ title: string; /** Navigation target for leaf items. Ignored for drilling when `links` is non-empty. */ href?: string; /** Child items. A non-empty array makes this item a drill-in button, not a link. */ links?: PushMenuItem[]; }; /** One entry in the level stack — the root level plus one per drill-in. */ type PushMenuLevel = { id: string; title: string; /** 1 for the root level, incrementing per drill-in. */ depth: number; items: PushMenuItem[]; /** The item whose activation opened this level. Absent on the root level. */ parentItem?: PushMenuItem; }; /** * Collapses a trail of level titles into a single "A › B › C" string, keeping * the ends and eliding the middle once it outgrows `maxLength`. Exported so an * app can render the same trail outside the menu (e.g. in a sheet header). * Prefixed with "PushMenu" to avoid colliding with a future breadcrumb * component in the package barrel. * * `maxLength` is a HARD bound on the returned string, measured in UTF-16 units * (`String.length`) — the one exception is a single level, whose title is * returned verbatim because there is nothing to collapse. Callers rendering the * trail in a fixed-width slot can rely on that. Middle-elision is attempted * first because it reads better; when it does not fit, the full trail is * truncated from the end. */ declare function generatePushMenuBreadcrumb(levels: { title: string; }[], maxLength?: number): string; type PushMenuProps = Omit, 'children' | 'title'> & { /** The menu tree. Item ids must be unique across the whole tree. */ navigation: PushMenuItem[]; /** * The app's current pathname. Leaf links whose `href` matches get * `aria-current="page"` and the active treatment. Replaces the nswds-app * source's `usePathname()` — the design system is framework-free, so the * router value is passed in rather than read from next/navigation. */ currentHref?: string; /** Root level title, shown in the header row. Also the default `aria-label`. */ title?: string; /** Fired when a leaf item (link or child-less button) is activated. */ onItemClick?: (item: PushMenuItem) => void; /** Fired after a forward/back slide settles on a level. */ onNavigate?: (level: PushMenuLevel, history: PushMenuLevel[]) => void; /** * Renders a close button in the header row when provided. Unlike the * nswds-app source — which only offered close on sub-levels, leaving the * root level uncloseable — the button renders on every level. */ onClose?: () => void; /** Show the "A › B › C" trail under the header on sub-levels. Defaults to `true`. */ showBreadcrumbs?: boolean; /** * Slide duration in milliseconds, defaulting to `PUSH_MENU_DURATION_MS` * (300). Drives both the `--push-menu-duration` custom property and the * state machine's timeouts — tune the slide here, never by overriding the * custom property (see the coupling note on the constant). */ durationMs?: number; /** Label for the Back button on sub-levels. Defaults to `'Back'`. */ backLabel?: string; /** Accessible label for the close button. Defaults to `'Close menu'`. */ closeLabel?: string; /** * Heading level for the per-level title, following `FooterNavColumn`'s * precedent (the nswds-app source used a `Heading` component this package * does not ship). Defaults to `2`; `1` is excluded because a menu panel * title is never the page's own title. */ headingLevel?: 2 | 3 | 4 | 5 | 6; /** * Visually-hidden suffix appended to the accessible name of a row that * drills into a submenu. Defaults to `'submenu'`. * * The chevron is `aria-hidden`, so without this a drill-in row and a leaf * link are indistinguishable to a screen reader: both announce as * ", button"/"link" with no hint that one replaces the panel and the * other leaves the page. Pass `null` to suppress it. */ submenuLabel?: React__default.ReactNode; /** * Shown in place of the row list when a level has no items. Defaults to * "No navigation items available."; pass `null` to render an empty level. * * An empty `navigation` array is a legitimate runtime state — unpublished * content, a permission-filtered menu, a failed fetch — as distinct from the * malformed data `warnIfNavigationMalformed` reports to the console, which * is compiled out in production. Without a message the drawer opens onto a * blank panel whose only affordance is the close button. */ emptyMessage?: React__default.ReactNode; /** * When the menu is below its root level, let Escape pop one level instead of * bubbling to an enclosing dialog. Defaults to `true`. * * A drill-down nested in a `Sheet` inherits the dialog's Escape-to-dismiss, * so a reader three levels deep loses both their position AND the drawer * from one keypress — and `navigateBack` is the only route back up (the * breadcrumb is decorative, and there is no swipe gesture). Popping one * level matches the back-out affordance the component actually offers. * Escape at the ROOT level always bubbles, so the drawer still closes the * way a dialog should. Set `false` for plain dialog semantics. */ escapeGoesBack?: boolean; ref?: React__default.Ref<HTMLElement>; }; /** * Multi-level slide-in-place drill-down menu ("push menu") for mobile * navigation, ported from nswds-app's `MultiLevelPushMenu`. Items with * children render as buttons that slide a new level in from the right; leaf * items render as links through `Link`, so apps can inject their framework * link component via `LinkProvider`. Fill height from its container — compose * it inside `SheetContent side="left"` for the classic mobile drawer. * * Accessibility contract (all improvements over the nswds-app source, which * got several of these wrong): * * - The root is a `nav` landmark named by `title` (override with `aria-label`). * - Non-current levels carry the `inert` attribute, so a hidden level's links * are neither tabbable nor exposed to assistive tech. The app left every * mounted level in the tab order, letting keyboard users tab into invisible * history levels (WCAG 2.2, 2.4.3 Focus Order). * - Focus moves with the level change (2.4.3): drilling forward focuses the * new level's Back button; going back focuses the item that opened the * level just left, tracked by item id and restored after the slide settles. * Without this, `inert` on the old level would silently drop focus to * `<body>`. The Back button is the only POINTER route back (the breadcrumb * is decorative), so it always renders on sub-levels — hiding it would make * drill-down one-way. * - Escape below the root level pops ONE level rather than dismissing an * enclosing dialog (`escapeGoesBack`, default true). Nested in a `Sheet` the * inherited dialog behaviour discarded both the reader's position and the * drawer on a single keypress; Escape at the root still closes normally. * - Row labels WRAP rather than truncate. Rows are `min-h-11` with * `items-center`, so a long label grows its row instead of losing its tail — * in a `w-3/4` drawer on a small phone the budget is roughly 25 characters, * which real government labels ("Births, deaths and marriages") exceed. The * level heading is the one exception (fixed-height header row) and carries a * `title` attribute instead. * - Drill-in rows append a visually-hidden `submenuLabel` to their accessible * name, so they no longer sound identical to leaf links. * - A single visually-hidden `aria-live="polite"` region at the root announces * the current level's title, suffixed with the level number below the root. * A live attribute on the per-level headings would not work: each level's * heading is freshly mounted, and newly-mounted live regions are not * reliably announced. * - Level lists are `<ul role="list">` — `list-style: none` strips list * semantics in some screen reader/browser pairings (notably VoiceOver), and * the explicit role restores them. * - Every row is at least 44px tall (2.5.8 Target Size); Back/close buttons * inherit `Button`'s 44px touch target. * - During the slide, rows get `pointer-events-none` and the state machine * ignores re-entrant navigation, but nothing is ever `disabled` — disabling * the focused Back button mid-animation would eject keyboard focus. * - Slides are CSS transforms applied under `motion-safe:`, replacing the app's * inline transition strings (which ignored reduced motion). * * Departures from the nswds-app source, beyond the above: * * - The footer/stats block (Level N, item counts, progress dots, lucide * icons) is cut, along with its `showStats`/`showFooter` props: it was demo * chrome for the sandbox, not part of a navigation component's job. The * breadcrumb trail it hosted moves under the header row. * - The rendered "Navigation Error" fallback for MALFORMED data is replaced * by a dev-only console warning (`warnIfNavigationMalformed`), mirroring * button.tsx's `warnIfIconButtonUnlabelled`. An EMPTY level is a different * case and does render — see `emptyMessage`. Empty is a legitimate runtime * state rather than a programming error, and the console warning is compiled * out in production regardless. * - The breadcrumb trail is `aria-hidden`: it repeats what the heading and * live region already announce, and "›" separators read poorly in AT. * * The level stack is seeded from `navigation`/`title` on mount; pass a `key` * to remount (and reset to the root level) if either changes at runtime. */ declare function PushMenu({ navigation, currentHref, title, onItemClick, onNavigate, onClose, showBreadcrumbs, durationMs, backLabel, closeLabel, headingLevel, submenuLabel, emptyMessage, escapeGoesBack, className, style, 'aria-label': ariaLabel, ref, ...props }: PushMenuProps): React__default.JSX.Element | null; export { PUSH_MENU_DURATION_MS, PushMenu, type PushMenuItem, type PushMenuLevel, type PushMenuProps, generatePushMenuBreadcrumb };