From 8b9118a9a03329816d90ea96c5b97cc87052bed1 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Wed, 28 Oct 2026 01:32:35 -0700 Subject: [PATCH] docs(SideNav, AppShell): FR6 and FR17 are implemented --- packages/core/src/AppShell/AppShell.spec.md | 39 +++++++++----------- packages/core/src/SideNav/SideNav.spec.md | 41 ++++++++++----------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/packages/core/src/AppShell/AppShell.spec.md b/packages/core/src/AppShell/AppShell.spec.md index 2c3d2cdc7c108..ffecca605b1c6 100644 --- a/packages/core/src/AppShell/AppShell.spec.md +++ b/packages/core/src/AppShell/AppShell.spec.md @@ -36,7 +36,7 @@ system_specs: [spec:AST-012, spec:AST-027] | Behavior | One page frame: skip link, then a header region, an inline side region, and one `main` region. Below the mobile breakpoint the side nav leaves the page and AppShell composes a mobile drawer from the nav slots, unless `mobileNav` disables or replaces it. | | End-user impact | Keyboard users get a skip link to the main region; screen-reader users get one banner and one main landmark; phone-width users reach the side nav through a toggle and drawer while page content, its input values, and focus survive the layout switch. When the side nav collapses or expands, the content's old and new wrap swap on the rail's clock instead of re-wrapping in one frame (FR17). | | Builder impact | None. Callers fill slots and choose height, variant, content padding, and mobile behavior. | -| Compatibility/readiness | Records shipped behavior except FR17's content swap; no prop, default, or export changes. Six current-state gaps, listed under Current-state impact: the content swap (FR17), page-level stacking values, a banner pinned against its docs, an unmeasured late header, a raw `content` drawer the toggle cannot reach, and a fill-height main region that is not a tab stop. | +| Compatibility/readiness | Records shipped behavior; no prop, default, or export changes. Five current-state gaps, listed under Current-state impact: page-level stacking values, a banner pinned against its docs, an unmeasured late header, a raw `content` drawer the toggle cannot reach, and a fill-height main region that is not a tab stop. | | Review checks | Reject a second `main` or banner landmark from AppShell, a skip link that is not the first focusable element or does not move focus to `main`, a breakpoint that treats equality as mobile, or remounting page content when the breakpoint changes. Also reject a content area that animates its width during a side nav collapse or expand (FR17). | | Governing rules | `spec:AST-012` FR8 (width points), `family:layout-regions` FR9, `architecture:theme-application` INV10, `spec:AST-027` FR1–FR5 (local stacking), `architecture:component-theming-surface`; `component:SideNav` FR6 and `spec:AST-105` FR8 (the content swap). | @@ -52,11 +52,11 @@ at a theme-resolved width point. Consumer usage lives in `AppShell.doc.mjs`. ## Compatibility and migration -- Released default preserved: `yes`, except FR17: `no` once it ships, since every - AppShell with an inline collapsible SideNav then swaps its content wrap on a - collapse or expand, with no prop to keep today's instant re-wrap -- Compatibility class: records shipped behavior, except FR17, a nonbreaking - motion addition (Current-state impact); no public prop, default, target, or export changes +- Released default preserved: `yes`, except FR17: `no`, since every AppShell with + an inline collapsible SideNav swaps its content wrap on a collapse or expand, + with no prop to keep the earlier instant re-wrap +- Compatibility class: records shipped behavior, including FR17, a nonbreaking + motion addition; no public prop, default, target, or export changes - Controlled/uncontrolled behavior: the mobile drawer's open state is uncontrolled by default and controlled when `mobileNav.isOpen` is defined (FR11) @@ -66,9 +66,6 @@ Consumer migration instructions belong in consumer docs and release notes. ## Current-state impact -- **The content area re-wraps in one frame (FR17).** FR17's swap is not - implemented: today the content re-wraps at once when the side nav collapses, - so FR17 adds motion rather than replacing a width animation. - **Shell stacking values are page-level, not local.** The AppShell root is not an isolation owner. In `height="auto"` the sticky header paints at `z-index: 1`, and the focused skip link paints at `z-index: 9999`, both @@ -281,18 +278,18 @@ reachability, not a decision that it must stay unthemeable. ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| ------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -| FR1–FR4, AR1 | `AppShell.test.tsx` slot, landmark, banner, and ref suites | Content only; each slot; mobile top bar with banner | A missing or duplicate landmark, or a nested banner, fails role queries. | `audit:AppShell/anatomy` | -| FR5, AR2 | `AppShell.test.tsx` skip-link and keyboard suites | First Tab; Enter; click; translated text | Losing the href, the focus move, or tab order fails. | `audit:AppShell/accessibility` | -| FR6, PR1 | `AppShell.test.tsx` height-mode suites | `fill`, `auto`, section header in `auto` | Dropping the header's sticky placement or measuring in `fill` fails. | `audit:AppShell/behavior` | -| FR8 | `AppShell.test.tsx` breakpoint suites | `lg`, `xl`, `2xl`, explicit `md`, `none`; nearest, root, and built themes; SSR hint | Wrong map, inclusive edge, or `none` entering mobile fails. | `audit:AppShell/behavior` | -| FR9–FR12 | `AppShell.test.tsx` mobile suites; `MobileNavCloseEdgeCases.test.tsx` AppShell drawer suites | Sidenav only; both navs; heading-only TopNav; custom node | A missing toggle, duplicated items, a stale drawer, or an early hide fails. | `audit:AppShell/behavior` | -| FR13, AR3 | `AppShell.test.tsx` keyboard drawer and `useAppShellMobile` suites | Outside a shell; inside, closed and open | A wrong id link or stale state fails. | `audit:AppShell/accessibility` | -| FR14, AR4 | `AppShell.test.tsx` breakpoint continuity test | Desktop to mobile and back with a focused input | Remounting content loses the value or focus. | `audit:AppShell/behavior` | -| FR17 | Real-browser frame evidence of the content area during a side nav collapse (pending) | collapse and expand with an inline side nav; Reduce Motion under a coarse and under a fine primary pointer; `fill` and a scrolled `auto` page; a theme with `reducedMotion: 'always'`; a second toggle while the swap runs; an expand whose old wrap is not copied | a visible gap between the rail and the content, a content area that animates its width or moves on its own clock, an old wrap reachable by assistive technology or left after the swap, travel under Reduce Motion, anything but an opacity-only cross-fade of 200 ms or less under Reduce Motion on a coarse pointer (DR2), or any animated property under Reduce Motion on a fine pointer (DR3) fails | `audit:AppShell/behavior` | -| FR7, FR16 | `AppShell.stories.tsx` and visual baselines | `elevated` with both navs, at desktop width | A changed `elevated` background or missing corner shows in visual review. | `audit:AppShell/visual` | -| Theming anatomy map | `scripts/check-knowledge.mjs` | Consumer anatomy and current targets | Missing, extra, stale, or multiply assigned mappings fail repository validation. | `audit:AppShell/theming` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | +| FR1–FR4, AR1 | `AppShell.test.tsx` slot, landmark, banner, and ref suites | Content only; each slot; mobile top bar with banner | A missing or duplicate landmark, or a nested banner, fails role queries. | `audit:AppShell/anatomy` | +| FR5, AR2 | `AppShell.test.tsx` skip-link and keyboard suites | First Tab; Enter; click; translated text | Losing the href, the focus move, or tab order fails. | `audit:AppShell/accessibility` | +| FR6, PR1 | `AppShell.test.tsx` height-mode suites | `fill`, `auto`, section header in `auto` | Dropping the header's sticky placement or measuring in `fill` fails. | `audit:AppShell/behavior` | +| FR8 | `AppShell.test.tsx` breakpoint suites | `lg`, `xl`, `2xl`, explicit `md`, `none`; nearest, root, and built themes; SSR hint | Wrong map, inclusive edge, or `none` entering mobile fails. | `audit:AppShell/behavior` | +| FR9–FR12 | `AppShell.test.tsx` mobile suites; `MobileNavCloseEdgeCases.test.tsx` AppShell drawer suites | Sidenav only; both navs; heading-only TopNav; custom node | A missing toggle, duplicated items, a stale drawer, or an early hide fails. | `audit:AppShell/behavior` | +| FR13, AR3 | `AppShell.test.tsx` keyboard drawer and `useAppShellMobile` suites | Outside a shell; inside, closed and open | A wrong id link or stale state fails. | `audit:AppShell/accessibility` | +| FR14, AR4 | `AppShell.test.tsx` breakpoint continuity test | Desktop to mobile and back with a focused input | Remounting content loses the value or focus. | `audit:AppShell/behavior` | +| FR17 | `SideNavRailMotion.test.tsx`, `animatedLayout.boundary.test.ts`; real-browser frame evidence of the content area during a side nav collapse (pending) | collapse and expand with an inline side nav; Reduce Motion under a coarse and under a fine primary pointer; `fill` and a scrolled `auto` page; a theme with `reducedMotion: 'always'`; a second toggle while the swap runs; an expand whose old wrap is not copied | a visible gap between the rail and the content, a content area that animates its width or moves on its own clock, an old wrap reachable by assistive technology or left after the swap, travel under Reduce Motion, anything but an opacity-only cross-fade of 200 ms or less under Reduce Motion on a coarse pointer (DR2), or any animated property under Reduce Motion on a fine pointer (DR3) fails | `audit:AppShell/behavior` | +| FR7, FR16 | `AppShell.stories.tsx` and visual baselines | `elevated` with both navs, at desktop width | A changed `elevated` background or missing corner shows in visual review. | `audit:AppShell/visual` | +| Theming anatomy map | `scripts/check-knowledge.mjs` | Consumer anatomy and current targets | Missing, extra, stale, or multiply assigned mappings fail repository validation. | `audit:AppShell/theming` | Read from source, with no focused test or baseline yet: diff --git a/packages/core/src/SideNav/SideNav.spec.md b/packages/core/src/SideNav/SideNav.spec.md index 0b46e51e977a8..57fffff02d8bf 100644 --- a/packages/core/src/SideNav/SideNav.spec.md +++ b/packages/core/src/SideNav/SideNav.spec.md @@ -67,9 +67,8 @@ a row's depth and its row padding. ## Compatibility and migration - Released default preserved: `no`; nested rows change from inset to full width. - Top-level rows and the collapsed rail are unchanged. FR6 is not implemented yet: today - the rail and its labels change in one frame, so FR6 adds the rail's clip - motion; it replaces no width animation. + Top-level rows and the collapsed rail are unchanged. The rail's + collapse and expand gain FR6's motion. - Compatibility class: nonbreaking appearance change of visual direction under `spec:AST-017/FR3`; no public API, accepted input, or accessibility semantic changes. @@ -113,14 +112,14 @@ Consumer migration instructions belong in consumer docs and release notes. ## Behavioral and layout contract -| ID | Invariant | Basis | Draft review state | -| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------- | -| FR1 | SideNav renders its navigation children in a scrollable list, which may be grouped by SideNavSection. A SideNavItem with children renders them, after its own row, in a group labelled by the parent's label; unless `collapsible` is `false`, the group expands and collapses and is hidden from assistive technology and inert while collapsed. | Current source, docs, and tests | Current behavior | -| FR2 | In a collapsed rail, a SideNavItem without an icon is not rendered; a leaf item renders icon-only, named by a non-blank caller `aria-label` or else its label; an item with children renders an icon-only trigger that opens a flyout listing its children as the flyout's top level. | Current source, docs, and tests | Current behavior | -| FR3 | In an expanded rail, in a flyout, and in the mobile navigation drawer, every SideNavItem row MUST span the full inline size of its list at any depth. A row at depth `n` MUST start its content `n` × `--spacing-6` beyond the row's own inline-start padding for its size, inside the row; the nested group MUST add no inset. A flyout's top level is depth 0. | `component:SideNav/DEC-1` | Settled intent; implementation may be staged | -| FR4 | The indent area MUST be part of the row's target: a click there, including a modified or middle click on a link row, does what the same click on the row's primary element does. When the row also carries an independent expand toggle or `actions`, the indent belongs to the primary link or button, and the toggle and actions keep their own activation at the row's trailing edge. | `component:SideNav/DEC-1`; `family:navigation-destinations` | Settled intent; implementation may be staged | -| FR5 | An expanded nested group MUST NOT clip its rows once it has finished opening, so a nested row's focus ring paints in full. The group MAY clip while collapsed and while it opens or closes. | `component:SideNav/DEC-1` | Settled intent; implementation may be staged | -| FR6 | Collapsing or expanding the rail plays one motion on one clock, `spec:AST-105` FR4's one shared motion (`springs.settle`): layout snaps to the final rail width at once, with no width animation, and the rail reveals or covers its labels by a clip: a collapse ends in the real collapsed state, icons only, and an expand ends in the real expanded state, with its labels. On a collapse or an expand the rail's old state is one transient copy, inert and hidden from assistive technology, removed when the motion ends. The main content moves on the same clock, as `component:AppShell` FR17 states. Under reduced motion the rail follows `design:reduced-motion` DR2 under a coarse primary pointer and DR3 otherwise. | `component:SideNav/DEC-2`; `component:AppShell` FR17; `spec:AST-105` FR8 | Not implemented | +| ID | Invariant | Basis | Draft review state | +| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| FR1 | SideNav renders its navigation children in a scrollable list, which may be grouped by SideNavSection. A SideNavItem with children renders them, after its own row, in a group labelled by the parent's label; unless `collapsible` is `false`, the group expands and collapses and is hidden from assistive technology and inert while collapsed. | Current source, docs, and tests | Current behavior | +| FR2 | In a collapsed rail, a SideNavItem without an icon is not rendered; a leaf item renders icon-only, named by a non-blank caller `aria-label` or else its label; an item with children renders an icon-only trigger that opens a flyout listing its children as the flyout's top level. | Current source, docs, and tests | Current behavior | +| FR3 | In an expanded rail, in a flyout, and in the mobile navigation drawer, every SideNavItem row MUST span the full inline size of its list at any depth. A row at depth `n` MUST start its content `n` × `--spacing-6` beyond the row's own inline-start padding for its size, inside the row; the nested group MUST add no inset. A flyout's top level is depth 0. | `component:SideNav/DEC-1` | Settled intent; implementation may be staged | +| FR4 | The indent area MUST be part of the row's target: a click there, including a modified or middle click on a link row, does what the same click on the row's primary element does. When the row also carries an independent expand toggle or `actions`, the indent belongs to the primary link or button, and the toggle and actions keep their own activation at the row's trailing edge. | `component:SideNav/DEC-1`; `family:navigation-destinations` | Settled intent; implementation may be staged | +| FR5 | An expanded nested group MUST NOT clip its rows once it has finished opening, so a nested row's focus ring paints in full. The group MAY clip while collapsed and while it opens or closes. | `component:SideNav/DEC-1` | Settled intent; implementation may be staged | +| FR6 | Collapsing or expanding the rail plays one motion on one clock, `spec:AST-105` FR4's one shared motion (`springs.settle`): layout snaps to the final rail width at once, with no width animation, and the rail reveals or covers its labels by a clip: a collapse ends in the real collapsed state, icons only, and an expand ends in the real expanded state, with its labels. On a collapse or an expand the rail's old state is one transient copy, inert and hidden from assistive technology, removed when the motion ends. The main content moves on the same clock, as `component:AppShell` FR17 states. Under reduced motion the rail follows `design:reduced-motion` DR2 under a coarse primary pointer and DR3 otherwise. | `component:SideNav/DEC-2`; `component:AppShell` FR17; `spec:AST-105` FR8 | Verified by `SideNavRailMotion.test.tsx`; real-browser frame evidence pending | ### Allowed variation @@ -196,16 +195,16 @@ Consumer migration instructions belong in consumer docs and release notes. ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| --------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | -| FR1, FR2 | `SideNav.test.tsx` nesting, collapsible, and collapsed-rail suites | Nested, collapsible, collapsed rail, flyout | Removing the labelled group, its collapsed inert state, or the collapsed icon-only and flyout paths fails. | `audit:SideNav/anatomy` | -| FR3, ORD1 | Focused `SideNav.test.tsx` indent coverage, required with implementation | Depth 1 and 2; `sm` and `md`; split and actions rows; caller `style`; flyout | A group inset, a missing or wrong indent, a dropped caller `style`, an indent that overrides a caller-set inline-start padding, or an indented flyout top level fails. | `audit:SideNav/layout` | -| FR3, DD1 | Real-browser geometry over the SideNav nested stories | Depths 1–3; `sm`, `md`, `lg`; light and dark; right-to-left; mobile drawer | A nested row's box narrower than a top-level row's, or a label moved from its per-depth position, fails. | `audit:SideNav/layout` | -| FR4 | Real-browser click in the indent area of plain, split, and actions rows | Link and button rows; plain, modified, and middle clicks; nested rows with toggle and actions | A click in the indent that does nothing or differs from the same click on the primary element, or a toggle or action that activates the row, fails. | `audit:SideNav/behavior` | -| FR5, AR2 | `SideNav.test.tsx` group clipping case and real-browser keyboard focus on nested rows | Only-child and last nested rows; after opening; collapsed | A focus ring cut at any edge of an expanded group, or an expanded group that still clips, fails. | `audit:SideNav/a11y` | -| FR2, FR3 | Real-browser collapsed rail and flyout | Collapsed rail with nested children | Any change to the collapsed rail, or an indent on the flyout's top level, fails. | `audit:SideNav/layout` | -| AR1 | Accessibility-tree comparison of nested rows | Plain, split, and actions rows | Any change in role, name, state, or order fails. | `audit:SideNav/a11y` | -| FR6 | Real-browser frame evidence of a collapse and an expand at desktop width (pending) | collapse and expand; a mouse and a finger; Reduce Motion under a coarse and under a fine primary pointer; a theme with `reducedMotion: 'always'`; a second toggle while the motion runs | an animated width, a collapse that does not end icons-only, an expand that does not end with its labels, a rail copy reachable by assistive technology or left after the motion ends, travel under Reduce Motion, anything but an opacity-only change of 200 ms or less under Reduce Motion on a coarse pointer (DR2), or any animated property under Reduce Motion on a fine pointer (DR3) fails | `audit:SideNav/behavior` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| --------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| FR1, FR2 | `SideNav.test.tsx` nesting, collapsible, and collapsed-rail suites | Nested, collapsible, collapsed rail, flyout | Removing the labelled group, its collapsed inert state, or the collapsed icon-only and flyout paths fails. | `audit:SideNav/anatomy` | +| FR3, ORD1 | Focused `SideNav.test.tsx` indent coverage, required with implementation | Depth 1 and 2; `sm` and `md`; split and actions rows; caller `style`; flyout | A group inset, a missing or wrong indent, a dropped caller `style`, an indent that overrides a caller-set inline-start padding, or an indented flyout top level fails. | `audit:SideNav/layout` | +| FR3, DD1 | Real-browser geometry over the SideNav nested stories | Depths 1–3; `sm`, `md`, `lg`; light and dark; right-to-left; mobile drawer | A nested row's box narrower than a top-level row's, or a label moved from its per-depth position, fails. | `audit:SideNav/layout` | +| FR4 | Real-browser click in the indent area of plain, split, and actions rows | Link and button rows; plain, modified, and middle clicks; nested rows with toggle and actions | A click in the indent that does nothing or differs from the same click on the primary element, or a toggle or action that activates the row, fails. | `audit:SideNav/behavior` | +| FR5, AR2 | `SideNav.test.tsx` group clipping case and real-browser keyboard focus on nested rows | Only-child and last nested rows; after opening; collapsed | A focus ring cut at any edge of an expanded group, or an expanded group that still clips, fails. | `audit:SideNav/a11y` | +| FR2, FR3 | Real-browser collapsed rail and flyout | Collapsed rail with nested children | Any change to the collapsed rail, or an indent on the flyout's top level, fails. | `audit:SideNav/layout` | +| AR1 | Accessibility-tree comparison of nested rows | Plain, split, and actions rows | Any change in role, name, state, or order fails. | `audit:SideNav/a11y` | +| FR6 | `SideNavRailMotion.test.tsx`; real-browser frame evidence of a collapse and an expand at desktop width (pending) | collapse and expand; a mouse and a finger; Reduce Motion under a coarse and under a fine primary pointer; a theme with `reducedMotion: 'always'`; a second toggle while the motion runs | an animated width, a collapse that does not end icons-only, an expand that does not end with its labels, a rail copy reachable by assistive technology or left after the motion ends, travel under Reduce Motion, anything but an opacity-only change of 200 ms or less under Reduce Motion on a coarse pointer (DR2), or any animated property under Reduce Motion on a fine pointer (DR3) fails | `audit:SideNav/behavior` | ## Decision log