@astryxdesign/core 0.7.2 → 0.7.3-canary.0bf23bd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  2. package/dist/BottomSheet/BottomSheet.js +8 -4
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
  5. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  6. package/dist/CheckboxInput/CheckboxInput.js +19 -24
  7. package/dist/ClickableCard/ClickableCard.d.ts +2 -2
  8. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  9. package/dist/ClickableCard/ClickableCard.js +11 -9
  10. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  11. package/dist/CodeBlock/CodeBlock.js +11 -1
  12. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  13. package/dist/ComplexSelector/ComplexSelector.js +23 -5
  14. package/dist/DateInput/DateInput.d.ts +4 -4
  15. package/dist/DateInput/DateInput.d.ts.map +1 -1
  16. package/dist/DateInput/DateInput.js +27 -20
  17. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  18. package/dist/DateInput/NativeDateField.js +14 -8
  19. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  20. package/dist/DateInput/TouchDateField.js +14 -10
  21. package/dist/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/Dialog/Dialog.js +9 -4
  23. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenu.js +52 -10
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
  26. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
  31. package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
  32. package/dist/DropdownMenu/menuRegionStops.js +2 -2
  33. package/dist/FileInput/FileInput.d.ts.map +1 -1
  34. package/dist/FileInput/FileInput.js +14 -12
  35. package/dist/Link/Link.d.ts +2 -3
  36. package/dist/Link/Link.d.ts.map +1 -1
  37. package/dist/Link/Link.js +11 -19
  38. package/dist/Markdown/Markdown.d.ts.map +1 -1
  39. package/dist/Markdown/Markdown.js +48 -15
  40. package/dist/MultiSelector/MultiSelector.d.ts +4 -4
  41. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  42. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
  43. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
  44. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
  45. package/dist/MultiSelector/MultiSelector.js +25 -18
  46. package/dist/Popover/Popover.d.ts.map +1 -1
  47. package/dist/Popover/Popover.js +8 -4
  48. package/dist/Selector/Selector.d.ts +4 -4
  49. package/dist/Selector/Selector.d.ts.map +1 -1
  50. package/dist/Selector/Selector.js +23 -14
  51. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  52. package/dist/Selector/SelectorOption.js +1 -0
  53. package/dist/Slider/Slider.d.ts.map +1 -1
  54. package/dist/Slider/Slider.js +7 -14
  55. package/dist/Switch/Switch.d.ts.map +1 -1
  56. package/dist/Switch/Switch.js +25 -17
  57. package/dist/Switch/switch.markers.stylex.d.ts +2 -0
  58. package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
  59. package/dist/Switch/switch.markers.stylex.js +6 -0
  60. package/dist/TimeInput/TimeInput.d.ts +2 -1
  61. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  62. package/dist/Token/Token.d.ts.map +1 -1
  63. package/dist/Token/Token.js +8 -3
  64. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  65. package/dist/Tooltip/Tooltip.js +36 -14
  66. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  67. package/dist/Tooltip/useTooltip.js +11 -2
  68. package/dist/TopNav/TopNav.d.ts.map +1 -1
  69. package/dist/TopNav/TopNav.js +2 -2
  70. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  71. package/dist/TopNav/TopNavHeading.js +1 -0
  72. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  73. package/dist/Typeahead/Typeahead.js +14 -2
  74. package/dist/astryx.css +8 -2
  75. package/dist/hooks/useMenuPress.d.ts +11 -1
  76. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  77. package/dist/hooks/useMenuPress.js +43 -5
  78. package/dist/utils/disabledInteraction.d.ts +21 -0
  79. package/dist/utils/disabledInteraction.d.ts.map +1 -0
  80. package/dist/utils/disabledInteraction.js +33 -0
  81. package/dist/utils/timeParser.d.ts +4 -0
  82. package/dist/utils/timeParser.d.ts.map +1 -1
  83. package/dist/utils/timeParser.js +9 -2
  84. package/package.json +4 -4
  85. package/scripts/agent-doc-state.mjs +35 -8
  86. package/src/Banner/Banner.spec.md +37 -32
  87. package/src/BottomSheet/BottomSheet.spec.md +13 -6
  88. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  89. package/src/BottomSheet/BottomSheet.tsx +8 -4
  90. package/src/BottomSheet/BottomSheetSwitcher.spec.md +121 -51
  91. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
  92. package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
  93. package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
  94. package/src/CheckboxInput/CheckboxInput.tsx +16 -26
  95. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
  96. package/src/CheckboxList/CheckboxList.test.tsx +9 -6
  97. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  98. package/src/ClickableCard/ClickableCard.test.tsx +5 -4
  99. package/src/ClickableCard/ClickableCard.tsx +9 -9
  100. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
  101. package/src/CodeBlock/CodeBlock.tsx +11 -1
  102. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
  103. package/src/ComplexSelector/ComplexSelector.tsx +21 -4
  104. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  105. package/src/DateInput/DateInput.doc.mjs +9 -8
  106. package/src/DateInput/DateInput.test.tsx +88 -13
  107. package/src/DateInput/DateInput.tsx +49 -21
  108. package/src/DateInput/DateInputTouch.test.tsx +22 -1
  109. package/src/DateInput/NativeDateField.test.tsx +29 -3
  110. package/src/DateInput/NativeDateField.tsx +20 -7
  111. package/src/DateInput/TouchDateField.tsx +24 -9
  112. package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
  113. package/src/Dialog/Dialog.test.tsx +39 -0
  114. package/src/Dialog/Dialog.tsx +9 -4
  115. package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
  116. package/src/DropdownMenu/DropdownMenu.tsx +79 -8
  117. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
  118. package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
  119. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
  120. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
  121. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
  122. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
  123. package/src/DropdownMenu/menuRegionStops.ts +2 -2
  124. package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
  125. package/src/FieldStatus/FieldStatus.spec.md +3 -2
  126. package/src/FileInput/FileInput.test.tsx +30 -7
  127. package/src/FileInput/FileInput.tsx +21 -11
  128. package/src/Link/Link.test.tsx +21 -7
  129. package/src/Link/Link.tsx +16 -24
  130. package/src/Link/__tests__/Link.a11y.states.ts +1 -1
  131. package/src/Markdown/Markdown.rerender.test.tsx +143 -0
  132. package/src/Markdown/Markdown.tsx +89 -35
  133. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
  134. package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
  135. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
  136. package/src/MultiSelector/MultiSelector.test.tsx +49 -7
  137. package/src/MultiSelector/MultiSelector.tsx +53 -29
  138. package/src/Popover/Popover.test.tsx +15 -0
  139. package/src/Popover/Popover.tsx +19 -8
  140. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
  141. package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
  142. package/src/Selector/Selector.doc.mjs +4 -4
  143. package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
  144. package/src/Selector/Selector.spec.md +11 -11
  145. package/src/Selector/Selector.test.tsx +64 -6
  146. package/src/Selector/Selector.tsx +51 -21
  147. package/src/Selector/SelectorOption.tsx +4 -0
  148. package/src/SideNav/SideNav.test.tsx +35 -0
  149. package/src/Slider/Slider.spec.md +11 -11
  150. package/src/Slider/Slider.test.tsx +3 -3
  151. package/src/Slider/Slider.tsx +8 -15
  152. package/src/Switch/Switch.test.tsx +85 -8
  153. package/src/Switch/Switch.tsx +26 -16
  154. package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
  155. package/src/Switch/switch.markers.stylex.ts +4 -0
  156. package/src/Table/Table.spec.md +2 -0
  157. package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
  158. package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
  159. package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
  160. package/src/TimeInput/TimeInput.doc.mjs +5 -3
  161. package/src/TimeInput/TimeInput.test.tsx +20 -0
  162. package/src/TimeInput/TimeInput.tsx +2 -1
  163. package/src/Token/Token.test.tsx +26 -4
  164. package/src/Token/Token.tsx +6 -2
  165. package/src/Tokenizer/Tokenizer.spec.md +28 -28
  166. package/src/Tooltip/Tooltip.test.tsx +51 -6
  167. package/src/Tooltip/Tooltip.tsx +41 -18
  168. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
  169. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
  170. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
  171. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
  172. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
  173. package/src/Tooltip/useTooltip.test.tsx +23 -0
  174. package/src/Tooltip/useTooltip.tsx +22 -9
  175. package/src/TopNav/TopNav.tsx +11 -1
  176. package/src/TopNav/TopNavHeading.tsx +1 -0
  177. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
  178. package/src/Typeahead/Typeahead.test.tsx +89 -0
  179. package/src/Typeahead/Typeahead.tsx +15 -2
  180. package/src/hooks/useMenuPress.ts +45 -7
  181. package/src/utils/disabledInteraction.ts +40 -0
  182. package/src/utils/timeParser.test.ts +11 -0
  183. package/src/utils/timeParser.ts +9 -2
@@ -13,6 +13,7 @@ review_triggers: [public-api, behavior, layout, theming, accessibility]
13
13
  verified_by:
14
14
  [
15
15
  packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx,
16
+ packages/core/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts,
16
17
  apps/storybook/stories/BottomSheetSwitcher.stories.tsx,
17
18
  scripts/check-knowledge.mjs,
18
19
  ]
@@ -34,16 +35,21 @@ system_specs: [spec:AST-002, spec:AST-027]
34
35
  ## Intent
35
36
 
36
37
  BottomSheetSwitcher coordinates a mutually exclusive sequence of BottomSheet
37
- children inside one shared native dialog. This draft records verified shipped and
38
- remediated behavior without adding a prop, changing a default, or settling the
39
- open modality, hosting, identifier, or theming decisions below.
38
+ children. This draft records verified shipped and remediated behavior, and the
39
+ hosting requirements its decision log decides — each child presented in its own
40
+ native dialog — without adding a prop, changing a default, or settling the
41
+ open modality, identifier, or theming decisions below. A row whose review state
42
+ is "Owner decision" is the decided requirement, not shipped behavior; the
43
+ representative states that name a sheet's own dialog, the retained dialog, or
44
+ the flow wrapper describe DEC-1's decided structure in the same way.
40
45
 
41
46
  ## Compatibility and migration
42
47
 
43
48
  - Released default preserved: `yes`
44
- - Compatibility class: observational backfill plus a shared-dismissal conformance
45
- fix; public props, defaults, exports, DOM ownership, and transition behavior stay
46
- unchanged
49
+ - Compatibility class: nonbreaking; public props, defaults, exports, visual
50
+ output, and transition behavior stay unchanged. The element the `ref` resolves
51
+ to, and that it changes on a handoff, is implementation rather than public
52
+ contract (DEC-2)
47
53
  - Controlled/uncontrolled behavior: `activeSheet` remains fully controlled
48
54
  - Migration decision: none
49
55
 
@@ -53,10 +59,16 @@ Consumer migration instructions belong in consumer docs and release notes.
53
59
 
54
60
  **Owns**
55
61
 
56
- - One shared native dialog for all directly nested BottomSheet children.
62
+ - The presentation lifecycle of every directly nested BottomSheet child's native
63
+ dialog: which is presented, retained during a handoff, or closed.
57
64
  - Which child sheet is active, retained during a handoff, or hidden.
58
- - Shared scrim, focus containment, scroll lock, Escape/platform-close routing,
59
- and final focus return for the flow.
65
+ - Which sheet dialog hosts the flow (its ref, `id`, and dismissal
66
+ registration) and which paints the flow's one scrim, plus
67
+ Escape/platform-close routing and final focus return for the flow. Focus
68
+ containment is the native modal dialog's; the flow's one page scroll lock is
69
+ held for the whole modal flow (DEC-1).
70
+ - The flow wrapper: the isolated surface that orders non-modal sheet dialogs
71
+ locally (`spec:AST-027` FR2).
60
72
 
61
73
  **Does not own / non-goals**
62
74
 
@@ -69,30 +81,30 @@ Consumer migration instructions belong in consumer docs and release notes.
69
81
 
70
82
  ## Public concepts
71
83
 
72
- | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
73
- | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
74
- | Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow | All presentations | `null` is closed; the prop is required | `component:BottomSheetSwitcher` | released | A non-matching or duplicate id is not given a safe public fallback by the current contract |
75
- | Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host | Entire flow | `true` | `component:BottomSheetSwitcher` | released | Boolean only |
76
- | Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child | Direct nested BottomSheet children | none | `component:BottomSheet` | released | Empty ids warn and stay hidden; duplicate-id behavior is not specified |
77
- | Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller | Escape/platform close, scrim click, or swipe according to active child purpose | none | `component:BottomSheetSwitcher`; `family:overlay-dismissal` owns ordering | released | The caller may retain its controlled value |
78
- | Dialog surface | inherited dialog props plus `ref` and `onCancel` | Reaches the one shared native dialog | Entire flow | none | `component:BottomSheetSwitcher` | released | Component-owned semantics and handlers retain precedence |
84
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
85
+ | ------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
86
+ | Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow | All presentations | `null` is closed; the prop is required | `component:BottomSheetSwitcher` | released | A non-matching or duplicate id is not given a safe public fallback by the current contract |
87
+ | Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host | Entire flow | `true` | `component:BottomSheetSwitcher` | released | Boolean only |
88
+ | Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child | Direct nested BottomSheet children | none | `component:BottomSheet` | released | Empty ids warn and stay hidden; duplicate-id behavior is not specified |
89
+ | Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller | Escape/platform close, scrim click, or swipe according to active child purpose | none | `component:BottomSheetSwitcher`; `family:overlay-dismissal` owns ordering | released | The caller may retain its controlled value |
90
+ | Dialog surface | inherited dialog props plus `ref` and `onCancel` | Dialog props and handlers, `onCancel` included, reach every sheet dialog; `ref`, `id`, and `data-testid` resolve to the host dialog, which is never null for an object ref while a sheet is mounted (owner decision, DEC-2) | Entire flow | none | `component:BottomSheetSwitcher` | released | Component-owned semantics and handlers retain precedence |
79
91
 
80
92
  ## Behavioral and layout contract
81
93
 
82
94
  Draft requirements identify their basis so observed code is not mistaken for an
83
95
  intentional decision. This draft cannot clear the gaps it records.
84
96
 
85
- | ID | Candidate invariant | Basis | Draft review state |
86
- | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------- |
87
- | FR1 | Zero or one child sheet is interactive for each controlled `activeSheet` value; all other children are hidden or retained inert and `aria-hidden` during a transition. | Current source, docs, and tests | Verified shipped behavior |
88
- | FR2 | All child sheets share one native dialog. Opening the first sheet opens that dialog once; handoffs do not replace it. | Current source, docs, and tests | Verified shipped behavior |
89
- | FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. | Current source and transition tests | Verified shipped behavior; exact visual treatment remains human-reviewed |
90
- | FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests | Verified shipped behavior |
91
- | FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, focus containment, and page scroll lock. `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. | Current source, docs, tests, and Chromium evidence | Verified shipped behavior; the non-modal host remains a `spec:AST-027` conformance gap |
92
- | FR6 | The active child purpose governs implicit dismissal: `info` allows Escape, scrim click, and swipe; `form` allows Escape; `required` blocks all three and exposes `alertdialog`. | Current BottomSheet docs and switcher tests | Verified shipped behavior |
93
- | FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test | Settled remediation in this audit |
94
- | FR8 | A stale gesture from an outgoing sheet cannot change the shared scrim, and unmounting a retained sheet cannot keep the shared dialog open. | Current tests | Verified shipped behavior |
95
- | FR9 | The switcher forwards its ref, neutral dialog attributes, styling inputs, and composed handlers to the shared dialog while preserving its owned ARIA and dismissal behavior. | Current source, public API architecture, and tests | Verified shipped behavior |
97
+ | ID | Candidate invariant | Basis | Draft review state |
98
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | FR1 | Zero or one child sheet is interactive for each controlled `activeSheet` value; all other children are hidden or retained inert and `aria-hidden` during a transition. | Current source, docs, and tests | Verified shipped behavior |
100
+ | FR2 | Each child sheet owns one native dialog. Opening a sheet presents its dialog once; a handoff presents the entering sheet's dialog above the retained one and closes the retained dialog only after its panel has faded. Returning to a sheet whose dialog is still open beneath a newer one re-presents it so the interactive sheet is the topmost layer. | DEC-1 | Owner decision |
101
+ | FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. | Current source and transition tests | Verified shipped behavior; exact visual treatment remains human-reviewed |
102
+ | FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests | Verified shipped behavior |
103
+ | FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (owner decision, DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive (verified current behavior). In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (owner decision, DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. | DEC-1; `spec:AST-027` FR2, FR4, FR8 | Modal host and the wrapper's local ordering: owner decision. Non-modal `show()` host: verified current behavior. The wrapper's page-level value remains a `spec:AST-027` conformance gap |
104
+ | FR6 | The active child purpose governs implicit dismissal: `info` allows Escape, scrim click, and swipe; `form` allows Escape; `required` blocks all three and exposes `alertdialog`. | Current BottomSheet docs and switcher tests | Verified shipped behavior |
105
+ | FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test | Settled remediation in this audit |
106
+ | FR8 | One backdrop paints the flow's scrim at a time: the retained sheet's dialog keeps it through a handoff and the entering dialog takes it over without a fade when the retained dialog closes. A stale gesture from an outgoing sheet cannot change the flow's scrim, and unmounting a retained sheet cannot keep its dialog open or the flow's modal layer held. | DEC-1; current tests | Owner decision |
107
+ | FR9 | The switcher forwards neutral dialog attributes, styling inputs, and composed handlers, `onCancel` included, to every sheet dialog, so a handler fires for whichever sheet dialog raises the event; renders `id` and `data-testid` on the host dialog only, so each appears once in the document; attaches its ref to the host dialog; and preserves its owned ARIA and dismissal behavior. An object ref's `current` is never `null` while a sheet is mounted. A callback ref follows React's semantics on a handoff: a cleanup it returned runs when it detaches from the old host; a callback ref without a cleanup is called with `null` on detach, then with the new host. | DEC-1, DEC-2; `architecture:public-component-api` INV8 | Owner decision |
96
108
 
97
109
  ### Allowed variation
98
110
 
@@ -104,14 +116,17 @@ intentional decision. This draft cannot clear the gaps it records.
104
116
 
105
117
  ### Representative states
106
118
 
107
- | State | Required invariant | Allowed variation |
108
- | ---------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
109
- | Closed | Shared dialog is closed and no child is interactive. | Child panels may remain mounted but hidden. |
110
- | First open | Matching child is visible and interactive in one shared dialog. | Child-owned height and content. |
111
- | Handoff | Entering child is interactive; retained child is visible but inert and hidden from accessibility APIs until it fades. | Alignment offset depends on measured panel geometry. |
112
- | Closing | Outgoing child is retained inert while exit motion completes; modal focus and scroll ownership remain until close. | Theme-controlled duration. |
113
- | Modal | Native modal host, backdrop, focus containment, and scroll lock are active. | Active child's `purpose` controls implicit dismissal. |
114
- | Non-modal | Native non-modal host has no backdrop or scroll lock and background content remains interactive. | The current clipping/stacking limitation is a conformance gap, not an allowed exception. |
119
+ The states below describe DEC-1's decided structure where they name a sheet's
120
+ own dialog, the retained dialog, or the flow wrapper.
121
+
122
+ | State | Required invariant | Allowed variation |
123
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
124
+ | Closed | Every sheet dialog is closed and no child is interactive. | Child panels may remain mounted but hidden. |
125
+ | First open | Matching child is visible and interactive in its own dialog. | Child-owned height and content. |
126
+ | Handoff | Entering child is interactive in its own dialog; retained child's dialog stays open, inert, and hidden from accessibility APIs until it fades, and keeps painting the one scrim. | Alignment offset depends on measured panel geometry. |
127
+ | Closing | Outgoing child is retained inert while exit motion completes; modal focus and scroll ownership remain until close. | Theme-controlled duration. |
128
+ | Modal | Native modal host, backdrop, native focus containment, and the page scroll lock are active. | Active child's `purpose` controls implicit dismissal. |
129
+ | Non-modal | Native non-modal host has no backdrop or scroll lock and background content remains interactive. | The current clipping/stacking limitation is a conformance gap, not an allowed exception. |
115
130
 
116
131
  ### Transformation and precedence order
117
132
 
@@ -124,8 +139,12 @@ intentional decision. This draft cannot clear the gaps it records.
124
139
 
125
140
  ### Performance and resources
126
141
 
127
- - **PR1 — One shared host.** A flow keeps one dialog, focus boundary, scroll lock,
128
- and backdrop across child handoffs rather than mounting a host for every child.
142
+ - **PR1 — One painted scrim.** In a modal flow, at rest exactly one sheet
143
+ dialog is presented; during a handoff two are, with one backdrop painted.
144
+ Native modality holds across a modal flow's child handoffs because a sheet
145
+ dialog is modal throughout, and the one page scroll lock is held for the
146
+ whole flow rather than per step. A non-modal flow paints no backdrop and
147
+ holds no lock.
129
148
  - **PR2 — Stable child registration.** Parent rerenders and changing consumer ref
130
149
  identities do not unregister a mounted child or cancel an active handoff.
131
150
 
@@ -141,11 +160,11 @@ intentional decision. This draft cannot clear the gaps it records.
141
160
 
142
161
  ## Design relationships
143
162
 
144
- | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
145
- | ---------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ | -------------- | ------------------ |
146
- | Shared dialog | Transparent host for the flow's interaction and accessibility boundary | Current source and public docs | Structural | FR2, FR5, AR1 |
147
- | Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet | `component:BottomSheet` draft as observational context | Prominent | FR1, FR3 |
148
- | Scrim | Optional native backdrop that dims and blocks the page in modal presentation | Current source and public docs | Supporting | FR5 |
163
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
164
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------- | ------------------ |
165
+ | Shared dialog | Transparent host for the flow's interaction and accessibility boundary; DEC-1 decides one such host per child sheet | Current source and public docs; DEC-1 for the per-sheet clause | Structural | FR2, FR5, AR1 |
166
+ | Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet | `component:BottomSheet` draft as observational context | Prominent | FR1, FR3 |
167
+ | Scrim | Optional native backdrop that dims and blocks the page in modal presentation; DEC-1 decides that one sheet dialog paints it at a time | Current source and public docs; DEC-1 for the one-painter clause | Supporting | FR5, FR8 |
149
168
 
150
169
  ### Theming anatomy
151
170
 
@@ -163,7 +182,7 @@ intentional decision. This draft cannot clear the gaps it records.
163
182
  },
164
183
  "Scrim": {
165
184
  "none": {
166
- "reason": "reachability-gap: The switcher-owned native backdrop paints the scrim but has no current public theming target."
185
+ "reason": "reachability-gap: The switcher-coordinated native backdrop paints the scrim but has no current public theming target."
167
186
  }
168
187
  }
169
188
  }
@@ -183,17 +202,68 @@ intentional decision. This draft cannot clear the gaps it records.
183
202
 
184
203
  ## Verification map
185
204
 
186
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
187
- | ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
188
- | FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close | A child becomes interactive at the wrong time, a retained child disappears early, or the shared host lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
189
- | FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; Chromium evidence | modal/non-modal, info/form/required, nested layer, IME, focus return | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges. | `audit:BottomSheetSwitcher/accessibility` |
190
- | FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
191
- | Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
205
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
206
+ | ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
207
+ | FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close | A child becomes interactive at the wrong time, a retained child disappears early, a second backdrop paints, or a sheet dialog's lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
208
+ | FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
209
+ | FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref across open/handoff/close/unmount, callback-ref cleanup, `id` once, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or duplicated, an object ref is null while a sheet is mounted or skips a cleanup, or an input overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
210
+ | Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
192
211
 
193
212
  ## Decision log
194
213
 
195
- None. This draft records observed behavior and one remediation already required by
196
- current shared authority; it introduces no component-local design or API decision.
214
+ ### DEC-1 — Each step owns its native dialog; the switcher coordinates
215
+
216
+ **Reference:** `component:BottomSheetSwitcher/DEC-1`
217
+ **Decider:** imdreamrunner, 2026-10-20
218
+
219
+ A switcher step is a BottomSheet with its own native dialog, presented and
220
+ closed like a standalone sheet, so one DOM shape serves standalone sheets and
221
+ switcher steps alike and the browser supplies modality per step: an inert
222
+ page, focus containment, `cancel` for Escape and the platform close, and the
223
+ backdrop. The switcher owns only what no single dialog can decide: which dialog
224
+ is presented, retained, or closed; the one scrim handed from the retained
225
+ dialog to the entering one without a fade; re-presenting a sheet that is still
226
+ open beneath a newer one when the flow returns to it; focus kept in the active
227
+ sheet when a retained dialog closes; focus returned to the flow's opener;
228
+ `cancel` gated by the active step's `purpose` through the shared dismissal
229
+ stack; and the flow's one page scroll lock, held from the moment a modal flow
230
+ is presented until after its exit, so a handoff between two open dialogs never
231
+ releases and re-acquires it. The switcher's one element is a flow wrapper with
232
+ no box in a modal flow and, in a non-modal flow, the isolated surface inside
233
+ which the sheet dialogs order themselves with local values.
234
+ Rejected: one switcher-owned dialog hosting every step as a panel, because a
235
+ step is then not a sheet and every standalone-sheet mechanism needs a second,
236
+ switcher-only implementation.
237
+ Rejected: a script focus trap on top of native containment, because the
238
+ browser already contains focus in a modal dialog and the trap fights its
239
+ inertness.
240
+ Rejected: a page-level `z-index` on a non-modal step's dialog, because
241
+ `spec:AST-027` FR4 forbids a new page band; the wrapper is the isolation owner.
242
+
243
+ ### DEC-2 — The host dialog's identity is implementation, not contract
244
+
245
+ **Reference:** `component:BottomSheetSwitcher/DEC-2`
246
+ **Decider:** imdreamrunner, 2026-10-20
247
+
248
+ The `ref`, `id`, and `data-testid` of the switcher reach the dialog hosting
249
+ the flow: the active sheet's, the exiting sheet's through its exit, and, while
250
+ the flow is closed, the dialog that hosted it last or the first mounted
251
+ sheet's, so an object ref is never `null` while a sheet is mounted and the
252
+ identifying attributes appear once. Which element that is, and that it changes
253
+ on a handoff, is implementation. The released documentation (v0.7.1
254
+ `BottomSheetSwitcher.doc.mjs`) describes the ref as "Ref forwarded to the one
255
+ shared native dialog." and the switcher as "Its ref and shared DOM props
256
+ target that dialog." Code that copies `ref.current` once at mount keeps a
257
+ real `<dialog>`, but after a handoff that element may no longer be the host;
258
+ read `ref.current` at use time, or use a callback ref, to follow the flow. DOM
259
+ props and handlers given to the switcher (`className`, `style`, `onCancel`,
260
+ `onToggle`, `aria-*`, and the rest) reach every sheet dialog, with `id` and
261
+ `data-testid` on the host only, so a handler fires for whichever sheet dialog
262
+ raises the event. The owner rules this hosting — the moving host and the
263
+ per-dialog props alike — nonbreaking under `spec:AST-017` FR3 because no
264
+ visual output and no public API surface changes.
265
+ Rejected: classifying the moving ref as a breaking change, because no public
266
+ API, documented interaction, or visual output changes.
197
267
 
198
268
  ## Open questions
199
269
 
@@ -708,6 +708,32 @@ describe('BottomSheetSwitcher', () => {
708
708
  expect(onActiveSheetChange).toHaveBeenNthCalledWith(2, null);
709
709
  });
710
710
 
711
+ it('keeps the flow open when a file picker inside a step is dismissed, and still closes on its own close request', () => {
712
+ // A file input fires a bubbling `cancel` at itself when its picker is
713
+ // dismissed. A step renders no dialog, so it reaches the switcher's.
714
+ const onActiveSheetChange = vi.fn();
715
+ render(
716
+ <BottomSheetSwitcher
717
+ activeSheet="upload"
718
+ onActiveSheetChange={onActiveSheetChange}>
719
+ <BottomSheet sheetId="upload" label="Upload" purpose="form">
720
+ <input type="file" aria-label="Picture" />
721
+ </BottomSheet>
722
+ </BottomSheetSwitcher>,
723
+ );
724
+
725
+ const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
726
+ fireEvent(screen.getByLabelText('Picture'), fromPicker);
727
+
728
+ expect(onActiveSheetChange).not.toHaveBeenCalled();
729
+ expect(fromPicker.defaultPrevented).toBe(false);
730
+
731
+ fireEvent(getSharedDialog(), new Event('cancel', {cancelable: true}));
732
+
733
+ expect(onActiveSheetChange).toHaveBeenCalledTimes(1);
734
+ expect(onActiveSheetChange).toHaveBeenCalledWith(null);
735
+ });
736
+
711
737
  it('honors purpose=required for a switcher-managed sheet', () => {
712
738
  const onActiveSheetChange = vi.fn();
713
739
  render(
@@ -591,8 +591,14 @@ export function BottomSheetSwitcher({
591
591
  ? undefined
592
592
  : sheetLabels.get(visibleTransition.retainedSheet));
593
593
 
594
+ // A `cancel` that bubbles up from a step is not a close request for the
595
+ // flow: a file input fires one when its picker is dismissed, and a step
596
+ // renders no dialog of its own, so it lands here. It is left alone.
594
597
  const handleCancel = useCallback(
595
598
  (event: SyntheticEvent<HTMLDialogElement>) => {
599
+ if (event.target !== event.currentTarget) {
600
+ return;
601
+ }
596
602
  event.preventDefault();
597
603
  if (shouldDismissOnCloseRequest()) {
598
604
  dismissOnEscape();
@@ -278,7 +278,7 @@ describe('CheckboxInput', () => {
278
278
  );
279
279
  const checkbox = screen.getByRole('checkbox');
280
280
  expect(checkbox).toBeChecked();
281
- expect(checkbox).toBeDisabled();
281
+ expect(checkbox).toHaveAttribute('aria-disabled', 'true');
282
282
  expect(checkbox).toHaveAttribute('type', 'checkbox');
283
283
  });
284
284
 
@@ -588,16 +588,55 @@ describe('CheckboxInput', () => {
588
588
  expect(checkbox).not.toBeChecked();
589
589
  });
590
590
 
591
- it('remains natively disabled when disabled without a reason', () => {
591
+ it('stays focusable via aria-disabled when disabled without a reason', async () => {
592
+ const user = userEvent.setup();
593
+ const onChange = vi.fn();
594
+ const onClick = vi.fn();
595
+ render(
596
+ <form>
597
+ <CheckboxInput
598
+ label="Accept terms"
599
+ value={false}
600
+ onChange={onChange}
601
+ onClick={onClick}
602
+ htmlName="terms"
603
+ isRequired
604
+ isDisabled
605
+ />
606
+ </form>,
607
+ );
608
+ const checkbox = screen.getByRole('checkbox');
609
+ expect(checkbox).not.toHaveAttribute('disabled');
610
+ expect(checkbox).toHaveAttribute('aria-disabled', 'true');
611
+ await user.tab();
612
+ expect(checkbox).toHaveFocus();
613
+ await user.keyboard(' ');
614
+ await user.click(checkbox);
615
+ expect(onChange).not.toHaveBeenCalled();
616
+ expect(onClick).not.toHaveBeenCalled();
617
+ expect(checkbox).not.toBeChecked();
618
+ const form = checkbox.closest('form')!;
619
+ expect(form.checkValidity()).toBe(true);
620
+ expect([...new FormData(form).keys()]).toEqual([]);
621
+ });
622
+
623
+ it('keeps a disabled indeterminate checkbox mixed after Space or a click', async () => {
624
+ const user = userEvent.setup();
592
625
  render(
593
626
  <CheckboxInput
594
- label="Accept terms"
595
- value={false}
627
+ label="Select all"
628
+ value="indeterminate"
596
629
  onChange={() => {}}
597
630
  isDisabled
598
631
  />,
599
632
  );
600
- expect(screen.getByRole('checkbox')).toBeDisabled();
633
+ const checkbox = screen.getByRole<HTMLInputElement>('checkbox');
634
+ expect(checkbox.indeterminate).toBe(true);
635
+ await user.tab();
636
+ await user.keyboard(' ');
637
+ fireEvent.click(checkbox);
638
+ expect(checkbox.indeterminate).toBe(true);
639
+ expect(checkbox).not.toBeChecked();
601
640
  });
602
641
  });
603
642
  describe('form participation', () => {
@@ -19,7 +19,6 @@
19
19
  import {
20
20
  useId,
21
21
  useCallback,
22
- use,
23
22
  useOptimistic,
24
23
  useRef,
25
24
  useTransition,
@@ -52,7 +51,6 @@ import {usePressFeedback} from '../hooks/usePressFeedback';
52
51
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
53
52
  import {useIndicator} from '../Indicator';
54
53
  import {themeProps} from '../utils/themeProps';
55
- import {CheckboxListContext} from '../CheckboxList/CheckboxListContext';
56
54
 
57
55
  import {useMergedRefs} from '../hooks/useMergedRefs';
58
56
 
@@ -354,21 +352,10 @@ export function CheckboxInput({
354
352
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
355
353
  const isBusy = isLoading || optimisticValue !== value;
356
354
 
357
- // Disabled-reason tooltip. Disabled controls swallow pointer events, so the
358
- // tooltip listeners attach to the checkbox row (which already exists) and the
359
- // native checkbox stays perceivable via aria-disabled instead of the disabled
360
- // attribute. Value mutation is blocked by the isDisabled guard in onChange.
355
+ // A disabled checkbox stays focusable via aria-disabled instead of the
356
+ // native disabled attribute (spec:AST-085 FR1). Value mutation is blocked by
357
+ // the click and onChange guards on the input.
361
358
  const showsDisabledMessage = isDisabled && !!disabledMessage;
362
- // Keep the native checkbox focusable via aria-disabled either when it renders
363
- // its own reason tooltip, or when it sits in a CheckboxList whose whole-group
364
- // `disabledMessage` (shown on the group container) needs each checkbox to
365
- // stay keyboard-perceivable. The group signals this through context rather
366
- // than a public prop.
367
- const checkboxListContext = use(CheckboxListContext);
368
- const isFocusableDisabled =
369
- isDisabled &&
370
- (showsDisabledMessage ||
371
- (checkboxListContext?.hasDisabledMessage ?? false));
372
359
  const disabledMessageTooltip = useTooltip({
373
360
  placement: 'above',
374
361
  // The container row is not naturally focusable; focusin bubbles up from the
@@ -384,7 +371,7 @@ export function CheckboxInput({
384
371
  // `opacity: 0`, and only the indicator's own element can shape the outline
385
372
  // to match it. See useIndicatorFocusRing.
386
373
  const indicatorRef = useRef<HTMLSpanElement>(null);
387
- const {focusProps} = useIndicatorFocusRing(indicatorRef, isDisabled);
374
+ const {focusProps} = useIndicatorFocusRing(indicatorRef);
388
375
 
389
376
  const isIndeterminate = optimisticValue === 'indeterminate';
390
377
  const isChecked = optimisticValue === true;
@@ -471,22 +458,25 @@ export function CheckboxInput({
471
458
  )}
472
459
  id={id}
473
460
  type="checkbox"
474
- // Withhold the name while disabled: with a disabledMessage the
475
- // input stays focusable (not natively disabled), and a disabled
476
- // control must not submit.
461
+ // Withhold the name while disabled: the input stays focusable (not
462
+ // natively disabled), and a disabled control must not submit.
477
463
  name={isDisabled ? undefined : htmlName}
478
464
  checked={isChecked}
479
- // With a disabledMessage the checkbox keeps focusability via
480
- // aria-disabled so the reason is focus-discoverable; toggling is
481
- // still blocked by the isDisabled guard in onChange below.
482
- disabled={isDisabled && !isFocusableDisabled}
483
- aria-disabled={isFocusableDisabled ? 'true' : undefined}
484
- form={isFocusableDisabled ? '' : undefined}
465
+ aria-disabled={isDisabled ? 'true' : undefined}
466
+ // Detached from its form while disabled, so it neither submits nor
467
+ // blocks submission through `required` (spec:AST-085 FR3).
468
+ form={isDisabled ? '' : undefined}
469
+ // Native `disabled` suppressed onClick (FR2); preventDefault here
470
+ // would desync React's controlled checked state.
471
+ onClick={isDisabled ? undefined : rest.onClick}
485
472
  readOnly={isReadOnly}
486
473
  required={isRequired}
487
474
  aria-required={isEffectivelyRequired ? 'true' : undefined}
488
475
  onChange={e => {
489
476
  if (isDisabled || isBusy || isReadOnly) {
477
+ // Activation clears the native mixed state; React restores
478
+ // only `checked`, so restore `indeterminate` too.
479
+ e.target.indeterminate = isIndeterminate;
490
480
  return;
491
481
  }
492
482
  const checked = e.target.checked;
@@ -125,8 +125,8 @@ export const CHECKBOX_BINDING_STATES = [
125
125
  {
126
126
  id: 'input-disabled',
127
127
  binding: 'CheckboxInput',
128
- summary: 'a natively disabled checkbox outside the tab sequence',
129
- facts: facts({operable: false, focusable: false, disabled: true}),
128
+ summary: 'a disabled checkbox that keeps its tab stop',
129
+ facts: facts({operable: false, focusable: true, disabled: true}),
130
130
  visibleLabel: 'Managed setting',
131
131
  storyId: 'a11y-checkbox-pattern--input-disabled',
132
132
  },
@@ -269,7 +269,7 @@ export const CHECKBOX_BINDING_STATES = [
269
269
  id: 'list-item-disabled',
270
270
  binding: 'CheckboxListItem',
271
271
  summary: 'an individually disabled list item',
272
- facts: facts({operable: false, focusable: false, disabled: true}),
272
+ facts: facts({operable: false, focusable: true, disabled: true}),
273
273
  visibleLabel: 'SMS',
274
274
  visibleLabelSelector: '[data-a11y-visible-label]',
275
275
  storyId: 'a11y-checkbox-pattern--list-item-disabled',
@@ -288,8 +288,9 @@ describe('CheckboxList', () => {
288
288
  </CheckboxList>,
289
289
  );
290
290
  const checkboxes = screen.getAllByRole('checkbox');
291
- expect(checkboxes[0]).toBeDisabled();
292
- expect(checkboxes[1]).toBeDisabled();
291
+ expect(checkboxes[0]).toHaveAttribute('aria-disabled', 'true');
292
+ expect(checkboxes[1]).toHaveAttribute('aria-disabled', 'true');
293
+ expect(checkboxes[0]).not.toHaveAttribute('disabled');
293
294
  });
294
295
 
295
296
  it('does not call onChange when group is disabled', () => {
@@ -317,8 +318,9 @@ describe('CheckboxList', () => {
317
318
  </CheckboxList>,
318
319
  );
319
320
  const checkboxes = screen.getAllByRole('checkbox');
320
- expect(checkboxes[0]).not.toBeDisabled();
321
- expect(checkboxes[1]).toBeDisabled();
321
+ expect(checkboxes[0]).not.toHaveAttribute('aria-disabled');
322
+ expect(checkboxes[1]).toHaveAttribute('aria-disabled', 'true');
323
+ expect(checkboxes[1]).not.toHaveAttribute('disabled');
322
324
  });
323
325
 
324
326
  it('throws when item has no value prop inside collection-mode CheckboxList', () => {
@@ -1137,7 +1139,7 @@ describe('CheckboxListItem ARIA props', () => {
1137
1139
  expect(onChange).not.toHaveBeenCalled();
1138
1140
  });
1139
1141
 
1140
- it('keeps checkboxes natively disabled when disabled without a reason', () => {
1142
+ it('keeps checkboxes focusable via aria-disabled when disabled without a reason', () => {
1141
1143
  render(
1142
1144
  <CheckboxList
1143
1145
  label="Notifications"
@@ -1149,7 +1151,8 @@ describe('CheckboxListItem ARIA props', () => {
1149
1151
  </CheckboxList>,
1150
1152
  );
1151
1153
  for (const checkbox of screen.getAllByRole('checkbox', h)) {
1152
- expect(checkbox).toBeDisabled();
1154
+ expect(checkbox).not.toHaveAttribute('disabled');
1155
+ expect(checkbox).toHaveAttribute('aria-disabled', 'true');
1153
1156
  }
1154
1157
  });
1155
1158
  });
@@ -24,7 +24,7 @@ export const docs = {
24
24
  {name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler: fires on card surface only.'},
25
25
  {name: 'href', type: 'string', description: 'Navigation URL. Plain, new-tab, Cmd/Ctrl-click, and middle-click activation all follow the shared navigation rule described on the Link `href` prop.'},
26
26
  {name: 'target', type: 'string', description: 'Link target.', default: "'_self'"},
27
- {name: 'isDisabled', type: 'boolean', description: 'Disables the card, removes a link destination, and leaves the card out of the tab order.', default: 'false'},
27
+ {name: 'isDisabled', type: 'boolean', description: 'Disables the card and removes a link destination; the card keeps its tab stop and is announced as disabled.', default: 'false'},
28
28
  {name: 'children', type: 'ReactNode', description: 'Card content.'},
29
29
  {name: 'padding', type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10', description: 'Inner padding.', default: '4'},
30
30
  {name: 'variant', type: "'default' | 'transparent' | 'muted' | 'blue' | 'cyan' | 'gray' | 'green' | 'orange' | 'pink' | 'purple' | 'red' | 'teal' | 'yellow'", description: 'Background color variant.', default: "'default'"},
@@ -75,7 +75,7 @@ export const docsDense = {
75
75
  onClick: 'click handler: fires on card surface only',
76
76
  href: 'navigation URL; every activation follows the shared navigation rule (see Link href)',
77
77
  target: 'link target',
78
- isDisabled: 'disables card, removes link destination, and leaves tab order',
78
+ isDisabled: 'disables card, removes link destination, keeps tab stop',
79
79
  padding: 'inner padding',
80
80
  variant: 'background color variant',
81
81
  elevation: 'resting shadow depth: none|low|med|high; often raised to signal clickability',