@astryxdesign/core 0.6.0 → 0.6.1

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 (203) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
  9. package/dist/Center/Center.d.ts +23 -16
  10. package/dist/Center/Center.d.ts.map +1 -1
  11. package/dist/Center/Center.js +7 -5
  12. package/dist/CodeBlock/CodeBlock.js +2 -2
  13. package/dist/DateInput/DateInput.d.ts.map +1 -1
  14. package/dist/DateInput/DateInput.js +12 -2
  15. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  16. package/dist/DateTimeInput/DateTimeInput.js +12 -2
  17. package/dist/Field/Field.d.ts.map +1 -1
  18. package/dist/Field/Field.js +1 -0
  19. package/dist/Field/InputClearButton.d.ts +2 -2
  20. package/dist/Field/InputClearButton.d.ts.map +1 -1
  21. package/dist/Field/InputClearButton.js +5 -1
  22. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  23. package/dist/Field/PanelSearchInput.js +16 -4
  24. package/dist/FileInput/FileInput.d.ts.map +1 -1
  25. package/dist/FileInput/FileInput.js +12 -1
  26. package/dist/HoverCard/useHoverCard.js +2 -2
  27. package/dist/Indicator/CheckboxIndicator.js +2 -2
  28. package/dist/Indicator/RadioIndicator.js +2 -2
  29. package/dist/Layer/layerStack.d.ts +10 -0
  30. package/dist/Layer/layerStack.d.ts.map +1 -1
  31. package/dist/Layer/layerStack.js +21 -9
  32. package/dist/Layer/useLayerDismissal.d.ts +2 -3
  33. package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
  34. package/dist/Layer/useLayerDismissal.js +2 -3
  35. package/dist/NavIcon/NavIcon.js +2 -2
  36. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  37. package/dist/NumberInput/NumberInput.js +12 -2
  38. package/dist/Popover/usePopover.d.ts +3 -2
  39. package/dist/Popover/usePopover.d.ts.map +1 -1
  40. package/dist/Popover/usePopover.js +4 -2
  41. package/dist/ProgressBar/ProgressBar.js +2 -2
  42. package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
  43. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
  44. package/dist/ScrollableArea/ScrollableArea.js +144 -0
  45. package/dist/ScrollableArea/index.d.ts +11 -0
  46. package/dist/ScrollableArea/index.d.ts.map +1 -0
  47. package/dist/ScrollableArea/index.js +11 -0
  48. package/dist/StatusDot/StatusDot.js +2 -2
  49. package/dist/TextArea/TextArea.js +2 -2
  50. package/dist/TextInput/TextInput.d.ts.map +1 -1
  51. package/dist/TextInput/TextInput.js +19 -4
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +12 -2
  54. package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
  55. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/BaseTypeahead.js +56 -20
  57. package/dist/astryx.css +15 -0
  58. package/dist/hooks/index.d.ts +2 -0
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/index.js +1 -0
  61. package/dist/hooks/scrollGeometry.d.ts +24 -0
  62. package/dist/hooks/scrollGeometry.d.ts.map +1 -0
  63. package/dist/hooks/scrollGeometry.js +86 -0
  64. package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
  65. package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
  66. package/dist/hooks/scrollOwnerRegistry.js +24 -0
  67. package/dist/hooks/useFocusTrap.d.ts +8 -0
  68. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  69. package/dist/hooks/useFocusTrap.js +22 -11
  70. package/dist/hooks/useScrollableArea.d.ts +51 -0
  71. package/dist/hooks/useScrollableArea.d.ts.map +1 -0
  72. package/dist/hooks/useScrollableArea.js +287 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/theme/defineTheme.d.ts +2 -6
  77. package/dist/theme/defineTheme.d.ts.map +1 -1
  78. package/dist/theme/defineTheme.js +1 -1
  79. package/dist/theme/derivedVarRegistry.js +1 -1
  80. package/dist/theme/localTokens.d.ts +8 -11
  81. package/dist/theme/localTokens.d.ts.map +1 -1
  82. package/dist/theme/localTokens.js +17 -71
  83. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  84. package/dist/theme/themeAdaptations.js +4 -4
  85. package/dist/utils/themeProps.d.ts +10 -10
  86. package/dist/utils/themeProps.d.ts.map +1 -1
  87. package/dist/utils/themeProps.js +27 -10
  88. package/locales/en.json +16 -0
  89. package/locales/pseudo.json +12 -0
  90. package/package.json +7 -2
  91. package/src/AppShell/AppShell.test.tsx +36 -0
  92. package/src/AppShell/AppShell.tsx +4 -1
  93. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  94. package/src/Banner/Banner.test.tsx +3 -1
  95. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
  96. package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
  97. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
  98. package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
  99. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
  100. package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
  101. package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
  102. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
  103. package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
  104. package/src/Center/Center.doc.mjs +32 -28
  105. package/src/Center/Center.spec.md +225 -0
  106. package/src/Center/Center.test.tsx +42 -4
  107. package/src/Center/Center.tsx +24 -17
  108. package/src/Chat/ChatSystemMessage.test.tsx +2 -9
  109. package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
  110. package/src/CodeBlock/CodeBlock.tsx +2 -2
  111. package/src/DateInput/DateInput.test.tsx +4 -4
  112. package/src/DateInput/DateInput.tsx +15 -4
  113. package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
  114. package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
  115. package/src/DateTimeInput/DateTimeInput.tsx +18 -7
  116. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
  117. package/src/Field/Field.test.tsx +42 -0
  118. package/src/Field/Field.tsx +6 -0
  119. package/src/Field/InputClearButton.test.tsx +35 -1
  120. package/src/Field/InputClearButton.tsx +7 -3
  121. package/src/Field/PanelSearchInput.tsx +21 -8
  122. package/src/FieldStatus/FieldStatus.spec.md +27 -17
  123. package/src/FieldStatus/FieldStatus.test.tsx +7 -5
  124. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
  125. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
  126. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
  127. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
  128. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
  129. package/src/FileInput/FileInput.tsx +10 -1
  130. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
  131. package/src/HoverCard/HoverCard.doc.mjs +4 -4
  132. package/src/HoverCard/useHoverCard.tsx +2 -2
  133. package/src/Indicator/CheckboxIndicator.tsx +2 -2
  134. package/src/Indicator/Indicator.doc.mjs +2 -2
  135. package/src/Indicator/Indicator.test.tsx +1 -1
  136. package/src/Indicator/RadioIndicator.tsx +2 -2
  137. package/src/Layer/layerStack.ts +20 -9
  138. package/src/Layer/useLayerDismissal.ts +2 -3
  139. package/src/MultiSelector/MultiSelector.test.tsx +4 -4
  140. package/src/NavIcon/NavIcon.doc.mjs +4 -4
  141. package/src/NavIcon/NavIcon.tsx +2 -2
  142. package/src/NumberInput/NumberInput.tsx +18 -7
  143. package/src/Popover/Popover.doc.mjs +10 -10
  144. package/src/Popover/Popover.spec.md +55 -65
  145. package/src/Popover/Popover.test.tsx +29 -0
  146. package/src/Popover/usePopover.doc.mjs +4 -4
  147. package/src/Popover/usePopover.tsx +7 -4
  148. package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
  149. package/src/ProgressBar/ProgressBar.test.tsx +1 -31
  150. package/src/ProgressBar/ProgressBar.tsx +2 -2
  151. package/src/RadioList/RadioList.test.tsx +5 -144
  152. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
  153. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
  154. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
  155. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
  156. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
  157. package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
  158. package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
  159. package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
  160. package/src/ScrollableArea/ScrollableArea.tsx +259 -0
  161. package/src/ScrollableArea/index.ts +26 -0
  162. package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
  163. package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
  164. package/src/Selector/Selector.test.tsx +4 -4
  165. package/src/Spinner/Spinner.test.tsx +0 -18
  166. package/src/StatusDot/StatusDot.doc.mjs +4 -4
  167. package/src/StatusDot/StatusDot.tsx +2 -2
  168. package/src/TabList/TabList.test.tsx +5 -9
  169. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
  170. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
  171. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
  172. package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
  173. package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
  174. package/src/Table/Table.doc.mjs +2 -2
  175. package/src/TextArea/TextArea.doc.mjs +4 -4
  176. package/src/TextArea/TextArea.tsx +2 -2
  177. package/src/TextInput/TextInput.doc.mjs +2 -1
  178. package/src/TextInput/TextInput.test.tsx +94 -0
  179. package/src/TextInput/TextInput.tsx +22 -6
  180. package/src/TimeInput/TimeInput.tsx +18 -7
  181. package/src/Toast/ToastViewport.test.tsx +1 -39
  182. package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
  183. package/src/Typeahead/BaseTypeahead.spec.md +269 -0
  184. package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
  185. package/src/Typeahead/BaseTypeahead.tsx +99 -30
  186. package/src/hooks/index.ts +13 -0
  187. package/src/hooks/scrollGeometry.ts +155 -0
  188. package/src/hooks/scrollOwnerRegistry.ts +47 -0
  189. package/src/hooks/useFocusTrap.ts +22 -11
  190. package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
  191. package/src/hooks/useScrollableArea.doc.mjs +108 -0
  192. package/src/hooks/useScrollableArea.test.tsx +437 -0
  193. package/src/hooks/useScrollableArea.ts +469 -0
  194. package/src/index.ts +1 -0
  195. package/src/theme/defineTheme.test.ts +65 -105
  196. package/src/theme/defineTheme.ts +3 -9
  197. package/src/theme/derivedVarRegistry.ts +1 -1
  198. package/src/theme/localTokens.ts +25 -96
  199. package/src/theme/publicThemeHelperContract.test.ts +2 -2
  200. package/src/theme/themeAdaptations.test.ts +16 -42
  201. package/src/theme/themeAdaptations.ts +6 -9
  202. package/src/utils/themeProps.test.ts +29 -10
  203. package/src/utils/themeProps.ts +36 -17
@@ -46,36 +46,33 @@ and rendering contract for custom compositions.
46
46
  This contract records the behavior present after
47
47
  [PR #5373](https://github.com/facebook/astryx/pull/5373), the Popover target
48
48
  direction settled by Cindy Zhang on 2026-08-31, and the focus/opening correction
49
- approved by Cindy Zhang on 2026-09-07. Public API signatures, emitted classes,
50
- and release status remain unchanged. Consumer syntax and complete signatures
51
- remain owned by `Popover.doc.mjs` and `usePopover.doc.mjs`; automatic canonical-
52
- target emission and compatibility removal require separately reviewed runtime
53
- work.
49
+ approved by Cindy Zhang on 2026-09-07. Public API signatures and release status
50
+ remain unchanged. Consumer syntax and complete signatures remain owned by
51
+ `Popover.doc.mjs` and `usePopover.doc.mjs`. The canonical `popover` target and
52
+ the deprecated `popover-surface` compatibility alias remain supported together.
54
53
 
55
54
  ## Compatibility and migration
56
55
 
57
56
  - Released default preserved: `yes`
58
- - Compatibility class: documentation-only target ownership clarification;
59
- runtime, DOM, styling, emitted classes, and public API are unchanged
57
+ - Compatibility class: the canonical and deprecated targets are emitted together;
58
+ runtime behavior, DOM shape, styling, and public API otherwise remain unchanged
60
59
  - Canonical target: `popover` owns the broad painted-surface contract for
61
- `<Popover>` and public `usePopover` compositions
62
- - Deprecated alias: `popover-surface` remains emitted so existing themes keep
63
- working during a documented migration window; it is not a second anatomy part
64
- or equal durable owner
60
+ `<Popover>` and public `usePopover` compositions and is preferred in new themes
61
+ - Deprecated alias: `popover-surface` remains emitted, documented as deprecated,
62
+ and discoverable with `popover` as its exact replacement so existing themes keep
63
+ working; it is not a second anatomy part or equal durable owner
65
64
  - Composed components: component-specific surface targets such as
66
65
  `selector-popup` and `multi-selector-popup` remain authoritative refinements;
67
- compatibility output may remain underneath until migration
66
+ the broad canonical and compatibility targets remain underneath
68
67
  - New hook consumers: a direct `usePopover` composition that needs its own theme
69
- reachability provides and documents an owned `surfaceTarget`; it does not
70
- create a new dependency on `popover-surface`
71
- - Migration decision: target ownership is settled; automatic `popover` emission
72
- for every painted hook surface and eventual alias removal ship only through
73
- separately reviewed runtime changes
68
+ reachability provides and documents an owned `surfaceTarget`; it does not create
69
+ a new dependency on `popover-surface`
70
+ - Migration decision: maintained themes and copyable new examples use `popover`;
71
+ compatibility discovery and runtime support for `popover-surface` continue
74
72
 
75
- The implementation migration must preserve `popover-surface` themes until its
76
- announced removal window. Implementation and removal each update consumer
77
- migration guidance, target inventory, focused tests, and changelog or release
78
- documentation; this documentation-only PR does none of those runtime steps.
73
+ Deprecation does not implement or require alias removal. CLI and theme-build
74
+ surfaces label `popover-surface` as deprecated and warn with the exact `popover`
75
+ replacement when it is authored, while existing themes continue to work.
79
76
 
80
77
  ## Ownership boundary
81
78
 
@@ -117,16 +114,16 @@ surface, not the Layer entry point. There is no installable
117
114
 
118
115
  ## Public concepts
119
116
 
120
- | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid or unsupported behavior |
121
- | ------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
- | Trigger composition | Wrapped trigger, render-prop trigger, or referenced anchor | Supplies the anchor and the control that opens or closes the standard component | Popover | Wrapped trigger when children are supplied | `component:Popover` | Current public behavior | A referenced or wrapped anchor without a button or button role warns in development and receives no trigger handler; it does not throw. |
123
- | Visibility ownership | Uncontrolled or externally controlled | Chooses whether Popover stores visibility or synchronizes to caller state | Popover | Uncontrolled | `component:Popover` | Current public behavior | Controlled changes are synchronized through show/hide; dismissal requests are reported to the caller rather than redefining the external source of truth. |
124
- | Popup semantics | Dialog or neutral wrapper | Exposes dialog semantics or lets child menu/listbox semantics own the popup | Popover and hook | Dialog | `component:Popover` | Current public behavior | A dialog without a label warns in development. Neutral mode omits dialog role and modal semantics. |
125
- | Focus entry | Automatic or caller-preserved | Focuses genuine caller content, falls back to the labeled dialog surface when none exists, or preserves current focus | Popover and hook | Automatic | `component:Popover` | Current public behavior | The injected fallback close control is excluded from initial-focus candidates and reveals only when reached sequentially. |
126
- | Dismissal | Outside/Escape enabled or disabled within native constraints | Controls light dismiss and explicit Escape participation | Popover and hook | Both enabled | `component:Popover`; `family:overlay-dismissal` owns Escape/platform-close ordering | Current public behavior | Disabling Escape alone cannot override the native Escape behavior of an auto popover; explicit-dismiss behavior requires light dismiss to be disabled too. |
127
- | Surface treatment | Default surface or caller-owned treatment | Applies the shared painted surface and optional consumer styling | Popover and hook | Default surface | `component:Popover` | Current public behavior | Custom styling does not change lifecycle, focus, or dismissal semantics. |
128
- | Surface target ownership | Shared baseline or component-specific refinement | Assigns `popover` as the broad surface owner and lets a composed component add its own target on that same element | Popover and hook | `popover` baseline; no component-specific refinement | `component:Popover`; the composed component owns its refinement target | Accepted contract; runtime alignment pending | `popover-surface` is compatibility output, not a new target for consumers or a second anatomy owner. |
129
- | Placement and fit | Logical placement/alignment plus preferred width | Positions the anchor surface and constrains it to available space | Popover and hook renderer | Below/start; Popover matches trigger minimum width when no width is supplied | `component:Popover` above `architecture:layer-runtime` | Current public behavior | Preferred width and trigger matching remain capped by viewport and safe-area availability. |
117
+ | Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid or unsupported behavior |
118
+ | ------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
119
+ | Trigger composition | Wrapped trigger, render-prop trigger, or referenced anchor | Supplies the anchor and the control that opens or closes the standard component | Popover | Wrapped trigger when children are supplied | `component:Popover` | Current public behavior | A referenced or wrapped anchor without a button or button role warns in development and receives no trigger handler; it does not throw. |
120
+ | Visibility ownership | Uncontrolled or externally controlled | Chooses whether Popover stores visibility or synchronizes to caller state | Popover | Uncontrolled | `component:Popover` | Current public behavior | Controlled changes are synchronized through show/hide; dismissal requests are reported to the caller rather than redefining the external source of truth. |
121
+ | Popup semantics | Dialog or neutral wrapper | Exposes dialog semantics or lets child menu/listbox semantics own the popup | Popover and hook | Dialog | `component:Popover` | Current public behavior | A dialog without a label warns in development. Neutral mode omits dialog role and modal semantics. |
122
+ | Focus entry | Automatic or caller-preserved | Focuses genuine caller content, falls back to the labeled dialog surface when none exists, or preserves current focus | Popover and hook | Automatic | `component:Popover` | Current public behavior | The injected fallback close control is excluded from initial-focus candidates and reveals only when reached sequentially. |
123
+ | Dismissal | Outside/Escape enabled or disabled within native constraints | Controls light dismiss and explicit Escape participation | Popover and hook | Both enabled | `component:Popover`; `family:overlay-dismissal` owns Escape/platform-close ordering | Current public behavior | Disabling Escape alone cannot override the native Escape behavior of an auto popover; explicit-dismiss behavior requires light dismiss to be disabled too. |
124
+ | Surface treatment | Default surface or caller-owned treatment | Applies the shared painted surface and optional consumer styling | Popover and hook | Default surface | `component:Popover` | Current public behavior | Custom styling does not change lifecycle, focus, or dismissal semantics. |
125
+ | Surface target ownership | Shared baseline or component-specific refinement | Assigns `popover` as the broad surface owner and lets a composed component add its own target on that same element | Popover and hook | `popover` baseline; no component-specific refinement | `component:Popover`; the composed component owns its refinement target | Current compatibility contract | `popover-surface` is compatibility output, not a new target for consumers or a second anatomy owner. |
126
+ | Placement and fit | Logical placement/alignment plus preferred width | Positions the anchor surface and constrains it to available space | Popover and hook renderer | Below/start; Popover matches trigger minimum width when no width is supplied | `component:Popover` above `architecture:layer-runtime` | Current public behavior | Preferred width and trigger matching remain capped by viewport and safe-area availability. |
130
127
 
131
128
  ### Public `usePopover` semantic inputs
132
129
 
@@ -173,15 +170,15 @@ table owns their semantic effect.
173
170
 
174
171
  ## Behavioral and layout contract
175
172
 
176
- | ID | Invariant | Basis | Acceptance and implementation state |
177
- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
178
- | FR1 | Popover and public `usePopover` expose one visibility lifecycle with canonical show, hide, and toggle operations. Every opening path inherits same-gesture reopen protection without changing public signatures. | Current public package surface, `architecture:layer-runtime/INV7`, and owner approval on 2026-09-07 | Accepted; implemented and covered by focused Popover-family tests |
179
- | FR2 | Popover derives semantic focus entry identically across activation modalities: first genuine caller content, then labeled dialog-surface fallback. The injected close control is excluded. Shared interaction modality controls focus indication, not destination. | PR #5373, `architecture:interaction-modality`, and owner approval on 2026-09-07 | Accepted; implemented and covered by Popover focus tests |
180
- | FR3 | The preferred surface size is capped to logical viewport and safe-area availability before overflow is enabled. | PR #5373, current source, tests, and Storybook fixtures | Verified in unit/style evidence; real rendered viewport evidence remains a gap |
181
- | FR4 | Popover enables internal scrolling only after measured overflow exceeds the current tolerance. Fitting content does not become a scroll container. | PR #5373 and current tests | Verified current behavior |
182
- | FR5 | Overflow signals while open coalesce into at most one measurement per animation frame, and Popover owns no measurement observers while closed. | PR #5373 and current tests | Verified current resource behavior |
183
- | FR6 | Component anatomy contains the caller trigger and content, one painted Popover surface, and the optional fallback close control. Popover owns no Header, Body, or separate shared-hook surface part. | Current source, docs target inventory, and tests | Accepted anatomy; stale consumer anatomy corrected by this contract |
184
- | FR7 | The painted surface has one broad canonical target, `popover`. `popover-surface` is a deprecated compatibility alias on that same part; composed components may add one authoritative component-specific refinement target. | Owner direction on 2026-08-31 plus current target inventory | Accepted semantic contract; automatic hook-wide emission and compatibility removal remain separately reviewed runtime work |
173
+ | ID | Invariant | Basis | Acceptance and implementation state |
174
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
175
+ | FR1 | Popover and public `usePopover` expose one visibility lifecycle with canonical show, hide, and toggle operations. Every opening path inherits same-gesture reopen protection without changing public signatures. | Current public package surface, `architecture:layer-runtime/INV7`, and owner approval on 2026-09-07 | Accepted; implemented and covered by focused Popover-family tests |
176
+ | FR2 | Popover derives semantic focus entry identically across activation modalities: first genuine caller content, then labeled dialog-surface fallback. The injected close control is excluded. Shared interaction modality controls focus indication, not destination. | PR #5373, `architecture:interaction-modality`, and owner approval on 2026-09-07 | Accepted; implemented and covered by Popover focus tests |
177
+ | FR3 | The preferred surface size is capped to logical viewport and safe-area availability before overflow is enabled. | PR #5373, current source, tests, and Storybook fixtures | Verified in unit/style evidence; real rendered viewport evidence remains a gap |
178
+ | FR4 | Popover enables internal scrolling only after measured overflow exceeds the current tolerance. Fitting content does not become a scroll container. | PR #5373 and current tests | Verified current behavior |
179
+ | FR5 | Overflow signals while open coalesce into at most one measurement per animation frame, and Popover owns no measurement observers while closed. | PR #5373 and current tests | Verified current resource behavior |
180
+ | FR6 | Component anatomy contains the caller trigger and content, one painted Popover surface, and the optional fallback close control. Popover owns no Header, Body, or separate shared-hook surface part. | Current source, docs target inventory, and tests | Accepted anatomy; stale consumer anatomy corrected by this contract |
181
+ | FR7 | The painted surface has one broad canonical target, `popover`. `popover-surface` remains a deprecated compatibility alias on that same part; composed components may add one authoritative component-specific refinement target. | Owner direction on 2026-08-31 plus current target inventory | Accepted compatibility contract; both canonical and deprecated paths remain supported and covered |
185
182
 
186
183
  ### Allowed variation
187
184
 
@@ -297,21 +294,20 @@ table owns their semantic effect.
297
294
 
298
295
  `popover-surface` is a deprecated alias for `popover` on the same painted
299
296
  surface. It is not anatomy and has no independent conceptual ownership. The
300
- component doc marks it with `deprecatedFor: 'popover'`, so the anatomy validator
301
- excludes it from the active target inventory while the runtime keeps emitting
302
- it for existing themes.
303
-
304
- Every public `usePopover` painted surface belongs to the broad `popover` target.
305
- A composed component may add its own authoritative refinement target, such as
306
- `selector-popup` or `multi-selector-popup`, on that same element. New direct
307
- hook consumers that need distinct theme reachability provide and document an
308
- owned `surfaceTarget`; they do not use `popover-surface`.
309
-
310
- Automatic hook-wide `popover` emission and eventual removal of the compatibility
311
- alias are runtime migration work outside this contract. Existing
312
- `popover-surface` themes remain supported until that separately reviewed
313
- migration includes target-inventory and regression-test updates plus changelog
314
- or release guidance.
297
+ component doc keeps `deprecatedFor: 'popover'`, discovery labels the alias with
298
+ that exact replacement, and runtime keeps emitting both names for existing
299
+ themes.
300
+
301
+ Every public `usePopover` painted surface belongs to the broad `popover` target
302
+ and retains the `popover-surface` compatibility alias. A composed component may
303
+ add its own authoritative refinement target, such as `selector-popup` or
304
+ `multi-selector-popup`, on that same element. New direct hook consumers that
305
+ need distinct theme reachability provide and document an owned `surfaceTarget`;
306
+ they use `popover` in new theme source rather than authoring the deprecated key.
307
+
308
+ Maintained themes, templates, and copyable new examples use `popover`. Existing
309
+ `popover-surface` themes remain supported; deprecation does not imply or require
310
+ removal.
315
311
 
316
312
  ## Family and system relationships
317
313
 
@@ -368,9 +364,9 @@ focus indication, while Popover owns semantic focus placement.
368
364
 
369
365
  `popover` is the broad canonical theming baseline automatically owned by every
370
366
  painted surface created by `<Popover>` or public `usePopover`.
371
- `popover-surface` is deprecated compatibility output on that same element, not a
372
- second anatomy part or equal owner. Existing themes using it remain supported
373
- through a documented migration window.
367
+ `popover-surface` remains deprecated compatibility output on that same element,
368
+ not a second anatomy part or equal owner. Existing themes using it remain
369
+ supported, while maintained and newly authored themes use `popover`.
374
370
 
375
371
  Composed components keep authoritative refinement targets such as
376
372
  `selector-popup` and `multi-selector-popup`. Optional `surfaceTarget` adds that
@@ -415,18 +411,12 @@ not block acceptance of the semantic contract above.
415
411
  - **VG2 — AT announcement.** The current story provides manual instructions, but
416
412
  named NVDA + Chrome and VoiceOver + Safari results are absent. No announcement
417
413
  outcome is claimed.
418
- - **VG3 — Target migration.** Current runtime still emits `popover-surface` as
419
- the automatic shared class and reaches `popover` through Popover's explicit
420
- `surfaceTarget`. A separate implementation must make `popover` automatic for
421
- every public hook surface while preserving the alias, then verify direct-hook
422
- and composed-component target inventories before any later removal.
423
414
 
424
415
  ## Open questions
425
416
 
426
417
  None. DEC-1 settles the visibility model, DEC-2 settles canonical target
427
418
  ownership, and DEC-3 settles semantic focus versus modality-aware indication.
428
- The remaining items are verification or separately reviewed implementation work,
429
- not human API questions.
419
+ The remaining items are verification work, not human API questions.
430
420
 
431
421
  ## Content boundary
432
422
 
@@ -17,6 +17,7 @@ import * as stylex from '@stylexjs/stylex';
17
17
  import {readFileSync} from 'node:fs';
18
18
  import React, {useRef} from 'react';
19
19
  import {Popover} from './Popover';
20
+ import {usePopover} from './usePopover';
20
21
  import type {UsePopoverReturn} from './usePopover';
21
22
  import {Dialog} from '../Dialog';
22
23
  import {SegmentedControl, SegmentedControlItem} from '../SegmentedControl';
@@ -83,6 +84,34 @@ describe('usePopover public return type', () => {
83
84
  expect(publicToggleTakesNoOptions).toBe(true);
84
85
  expect(hasInternalFocusTarget).toBe(false);
85
86
  });
87
+
88
+ it('emits canonical and deprecated surface targets for direct hook consumers', () => {
89
+ function HeadlessPopover() {
90
+ const popover = usePopover({dialogLabel: 'Headless popover'});
91
+ return (
92
+ <>
93
+ <button
94
+ type="button"
95
+ ref={popover.triggerRef}
96
+ onClick={() => popover.show()}
97
+ {...popover.triggerProps}>
98
+ Open headless
99
+ </button>
100
+ {popover.render(<span>Headless content</span>)}
101
+ </>
102
+ );
103
+ }
104
+
105
+ render(<HeadlessPopover />);
106
+ fireEvent.click(screen.getByRole('button', {name: 'Open headless'}));
107
+
108
+ const surface = screen.getByRole('dialog', {
109
+ name: 'Headless popover',
110
+ hidden: true,
111
+ });
112
+ expect(surface).toHaveClass('astryx-popover');
113
+ expect(surface).toHaveClass('astryx-popover-surface');
114
+ });
86
115
  });
87
116
 
88
117
  describe('Popover', () => {
@@ -159,7 +159,7 @@ export const docs = {
159
159
  ],
160
160
  usage: {
161
161
  description:
162
- 'Headless hook for click-triggered popovers with focus trapping. Combines useLayer with useFocusTrap, auto-focus, light dismiss, Escape handling, and an optional hidden close button for accessible dialog-like popover behavior. Use for custom interactive floating content that needs keyboard navigation. The canonical broad surface target is popover; automatic hook-wide emission is separate migration work, so current direct compositions needing a distinct stable seam should pass and document their own surfaceTarget instead of depending on deprecated popover-surface.',
162
+ 'Headless hook for click-triggered popovers with focus trapping. Combines useLayer with useFocusTrap, auto-focus, light dismiss, Escape handling, and an optional hidden close button for accessible dialog-like popover behavior. Every painted surface emits the canonical popover target and deprecated popover-surface compatibility alias. A custom composition needing a distinct stable seam should pass and document its own surfaceTarget.',
163
163
  bestPractices: [
164
164
  {
165
165
  guidance: true,
@@ -174,7 +174,7 @@ export const docs = {
174
174
  {
175
175
  guidance: true,
176
176
  description:
177
- 'Use popover as the broad surface target. During migration, popover-surface remains compatibility output only; do not create new theme dependencies on it.',
177
+ 'Use popover as the broad surface target. Popover-surface remains supported compatibility output, but new theme source uses the canonical key.',
178
178
  },
179
179
  {
180
180
  guidance: true,
@@ -197,7 +197,7 @@ export const docs = {
197
197
  /** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
198
198
  export const docsDense = {
199
199
  description:
200
- 'Headless click-triggered popovers w/ focus trap, auto-focus, light dismiss, Escape, optional hidden close button. Canonical broad target is popover; automatic hook-wide emission is separate migration work. Current direct compositions needing a distinct seam pass/document an owned surfaceTarget; do not depend on deprecated popover-surface.',
200
+ 'Headless click-triggered popovers w/ focus trap, auto-focus, light dismiss, Escape, optional hidden close button. Every surface emits canonical popover plus deprecated popover-surface compatibility output. Custom compositions needing a distinct seam pass/document an owned surfaceTarget.',
201
201
  paramDescriptions: {
202
202
  onShow: 'fires when popover becomes visible.',
203
203
  onHide: 'fires when popover hides; use to return focus when needed.',
@@ -230,7 +230,7 @@ export const docsDense = {
230
230
  },
231
231
  usage: {
232
232
  description:
233
- 'Headless click-triggered popovers w/ focus trap, auto-focus, light dismiss, Escape, optional hidden close button. Canonical broad target is popover; automatic hook-wide emission is separate migration work. Current direct compositions needing a distinct seam pass/document an owned surfaceTarget; do not depend on deprecated popover-surface.',
233
+ 'Headless click-triggered popovers w/ focus trap, auto-focus, light dismiss, Escape, optional hidden close button. Every surface emits canonical popover plus deprecated popover-surface compatibility output. Custom compositions needing a distinct seam pass/document an owned surfaceTarget.',
234
234
  bestPractices: [
235
235
  {
236
236
  guidance: true,
@@ -240,8 +240,9 @@ export interface UsePopoverOptions {
240
240
  * or radius rule paints the wrong box. Name the surface through this option
241
241
  * and document the class in the component's `theming.targets`.
242
242
  *
243
- * The shared `astryx-popover-surface` class is always present alongside it,
244
- * so a theme can style every popup surface at once.
243
+ * The canonical `astryx-popover` target and deprecated
244
+ * `astryx-popover-surface` compatibility alias are always present alongside
245
+ * any component-owned refinement target.
245
246
  */
246
247
  surfaceTarget?: string;
247
248
  }
@@ -485,9 +486,11 @@ function usePopoverImplementation(
485
486
  // `mergeProps` is positional — a third OBJECT argument is read as
486
487
  // `style`, not as more props — so the surface's classes are composed
487
488
  // into one props object before merging with the StyleX result.
488
- const surfaceProps = themeProps('popover-surface');
489
+ const surfaceProps = themeProps('popover', undefined, {
490
+ legacyNames: ['popover-surface'],
491
+ });
489
492
  const surfaceClassName =
490
- surfaceTarget != null
493
+ surfaceTarget != null && surfaceTarget !== 'popover'
491
494
  ? `${surfaceProps.className} ${stableClassName(surfaceTarget)}`
492
495
  : surfaceProps.className;
493
496
 
@@ -132,8 +132,8 @@ export const docs = {
132
132
  className: 'astryx-progress-bar-mark',
133
133
  visualProps: ['variant', 'placement'],
134
134
  },
135
- // Still emitted beside the names above, so themes written against
136
- // them keep working. Drop in the next major.
135
+ // Retained beside the canonical names for backwards compatibility.
136
+ // New themes use the canonical targets above.
137
137
  {
138
138
  className: 'astryx-progressbar',
139
139
  visualProps: ['variant'],
@@ -297,8 +297,8 @@ export const docsZh = {
297
297
  className: 'astryx-progress-bar-mark',
298
298
  visualProps: ['variant', 'placement'],
299
299
  },
300
- // Still emitted beside the names above, so themes written against
301
- // them keep working. Drop in the next major.
300
+ // Retained beside the canonical names for backwards compatibility.
301
+ // New themes use the canonical targets above.
302
302
  {
303
303
  className: 'astryx-progressbar',
304
304
  visualProps: ['variant'],
@@ -14,23 +14,6 @@ beforeAll(async () => {
14
14
  });
15
15
 
16
16
  describe('ProgressBar', () => {
17
- it('renders with default props', () => {
18
- render(<ProgressBar value={50} label="Progress" />);
19
- const progressbar = screen.getByRole('progressbar');
20
- expect(progressbar).toBeInTheDocument();
21
- expect(progressbar).toHaveAttribute('aria-valuenow', '50');
22
- expect(progressbar).toHaveAttribute('aria-valuemin', '0');
23
- expect(progressbar).toHaveAttribute('aria-valuemax', '100');
24
- });
25
-
26
- it('uses role="progressbar" (not "meter") for determinate progress', () => {
27
- // A determinate ProgressBar conveys task completion, so it must be a
28
- // progressbar (announced on update), not a meter (a static gauge).
29
- render(<ProgressBar value={50} label="Progress" />);
30
- expect(screen.getByRole('progressbar')).toBeInTheDocument();
31
- expect(screen.queryByRole('meter')).not.toBeInTheDocument();
32
- });
33
-
34
17
  it('renders visible label by default', () => {
35
18
  render(<ProgressBar value={50} label="Storage used" />);
36
19
  expect(screen.getByText('Storage used')).toBeInTheDocument();
@@ -70,13 +53,6 @@ describe('ProgressBar', () => {
70
53
  expect(progressbar).toHaveAttribute('aria-valuetext', '50%');
71
54
  });
72
55
 
73
- it('respects custom max', () => {
74
- render(<ProgressBar value={3} max={10} label="Steps" />);
75
- const progressbar = screen.getByRole('progressbar');
76
- expect(progressbar).toHaveAttribute('aria-valuenow', '3');
77
- expect(progressbar).toHaveAttribute('aria-valuemax', '10');
78
- });
79
-
80
56
  it('clamps value to [0, max]', () => {
81
57
  const {rerender} = render(
82
58
  <ProgressBar value={150} max={100} label="Over" />,
@@ -197,13 +173,7 @@ describe('ProgressBar', () => {
197
173
 
198
174
  // Indeterminate mode tests
199
175
  describe('indeterminate mode', () => {
200
- it('renders with role="progressbar" when isIndeterminate', () => {
201
- render(<ProgressBar isIndeterminate label="Loading" />);
202
- const progressbar = screen.getByRole('progressbar');
203
- expect(progressbar).toBeInTheDocument();
204
- });
205
-
206
- it('does not set aria-valuenow/min/max when indeterminate', () => {
176
+ it('omits authored value attributes while the value is indeterminate', () => {
207
177
  render(<ProgressBar isIndeterminate label="Loading" />);
208
178
  const progressbar = screen.getByRole('progressbar');
209
179
  expect(progressbar).not.toHaveAttribute('aria-valuenow');
@@ -482,8 +482,8 @@ export function ProgressBar({
482
482
  themeProps(
483
483
  'progress-bar',
484
484
  {variant},
485
- // `progressbar` ran the compound name together; themes styling it
486
- // keep working until the next major.
485
+ // `progressbar` ran the compound name together; keep it emitted so
486
+ // existing themes continue to work.
487
487
  {legacyNames: ['progressbar']},
488
488
  ),
489
489
  stylex.props(styles.container, xstyle),
@@ -3,8 +3,11 @@
3
3
  /**
4
4
  * @file RadioList.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, RadioList, RadioListItem
6
- * @output Unit tests for RadioList and RadioListItem behavior
7
- * @position Testing; validates RadioList.tsx and RadioListItem.tsx implementation
6
+ * @output Component-specific callback, form, composition, layout, and styling
7
+ * tests. Shared radio-group role, name, state, focus, and interaction outcomes
8
+ * live in __tests__/RadioGroup.a11y.test.tsx and its Chromium twin.
9
+ * @position Component-owned regression tests that do not duplicate the reusable
10
+ * radio-group contract.
8
11
  *
9
12
  * SYNC: When RadioList.tsx or RadioListItem.tsx changes, update tests to match new behavior
10
13
  */
@@ -64,29 +67,6 @@ describe('RadioList', () => {
64
67
  expect(screen.getAllByRole('radio')).toHaveLength(3);
65
68
  });
66
69
 
67
- it('renders radiogroup role', () => {
68
- render(
69
- <RadioList label="Preference" value="" onChange={() => {}}>
70
- <RadioListItem label="Option A" value="a" />
71
- </RadioList>,
72
- );
73
- expect(screen.getByRole('radiogroup')).toBeInTheDocument();
74
- });
75
-
76
- it('selects the correct radio based on value prop', () => {
77
- render(
78
- <RadioList label="Preference" value="b" onChange={() => {}}>
79
- <RadioListItem label="Option A" value="a" />
80
- <RadioListItem label="Option B" value="b" />
81
- <RadioListItem label="Option C" value="c" />
82
- </RadioList>,
83
- );
84
- const radios = screen.getAllByRole('radio');
85
- expect(radios[0]).not.toBeChecked();
86
- expect(radios[1]).toBeChecked();
87
- expect(radios[2]).not.toBeChecked();
88
- });
89
-
90
70
  it('calls onChange with value string when clicking a radio', async () => {
91
71
  const user = userEvent.setup();
92
72
  const handleChange = vi.fn();
@@ -132,18 +112,6 @@ describe('RadioList', () => {
132
112
  expect(handleChange).toHaveBeenCalledWith('b');
133
113
  });
134
114
 
135
- it('disables all radios when group isDisabled is true', () => {
136
- render(
137
- <RadioList label="Preference" value="" onChange={() => {}} isDisabled>
138
- <RadioListItem label="Option A" value="a" />
139
- <RadioListItem label="Option B" value="b" />
140
- </RadioList>,
141
- );
142
- const radios = screen.getAllByRole('radio');
143
- expect(radios[0]).toBeDisabled();
144
- expect(radios[1]).toBeDisabled();
145
- });
146
-
147
115
  it('does not call onChange when group is disabled', async () => {
148
116
  const user = userEvent.setup();
149
117
  const handleChange = vi.fn();
@@ -163,18 +131,6 @@ describe('RadioList', () => {
163
131
  expect(handleChange).not.toHaveBeenCalled();
164
132
  });
165
133
 
166
- it('disables individual item when item isDisabled is true', () => {
167
- render(
168
- <RadioList label="Preference" value="" onChange={() => {}}>
169
- <RadioListItem label="Option A" value="a" />
170
- <RadioListItem label="Option B" value="b" isDisabled />
171
- </RadioList>,
172
- );
173
- const radios = screen.getAllByRole('radio');
174
- expect(radios[0]).not.toBeDisabled();
175
- expect(radios[1]).toBeDisabled();
176
- });
177
-
178
134
  it('does not call onChange when individual item is disabled', async () => {
179
135
  const user = userEvent.setup();
180
136
  const handleChange = vi.fn();
@@ -251,22 +207,6 @@ describe('RadioList', () => {
251
207
  expect(screen.getByText('Great choice!')).toBeInTheDocument();
252
208
  });
253
209
 
254
- it('sets aria-invalid on radiogroup when status is error', () => {
255
- render(
256
- <RadioList
257
- label="Preference"
258
- value=""
259
- onChange={() => {}}
260
- status={{type: 'error', message: 'Required'}}>
261
- <RadioListItem label="Option A" value="a" />
262
- </RadioList>,
263
- );
264
- expect(screen.getByRole('radiogroup')).toHaveAttribute(
265
- 'aria-invalid',
266
- 'true',
267
- );
268
- });
269
-
270
210
  it('renders startContent', () => {
271
211
  render(
272
212
  <RadioList label="Preference" value="" onChange={() => {}}>
@@ -398,59 +338,7 @@ describe('RadioList', () => {
398
338
  expect(screen.getAllByRole('radio')).toHaveLength(2);
399
339
  });
400
340
 
401
- it('sets aria-required on radiogroup when isRequired is true', () => {
402
- render(
403
- <RadioList label="Preference" value="" onChange={() => {}} isRequired>
404
- <RadioListItem label="Option A" value="a" />
405
- </RadioList>,
406
- );
407
- expect(screen.getByRole('radiogroup')).toHaveAttribute(
408
- 'aria-required',
409
- 'true',
410
- );
411
- });
412
-
413
341
  describe('focus management (no-selection tab stop)', () => {
414
- it('keeps focus on the selected radio when a value is selected', () => {
415
- render(
416
- <RadioList label="Preference" value="b" onChange={() => {}}>
417
- <RadioListItem label="Option A" value="a" />
418
- <RadioListItem label="Option B" value="b" />
419
- <RadioListItem label="Option C" value="c" />
420
- </RadioList>,
421
- );
422
- const selected = screen.getByLabelText('Option B');
423
- // A selected value provides a deterministic native tab stop; focusing it
424
- // must not be redirected elsewhere.
425
- selected.focus();
426
- expect(selected).toHaveFocus();
427
- });
428
-
429
- it('redirects to the first radio when focus enters an unselected group forward', () => {
430
- render(
431
- <>
432
- <button type="button">before</button>
433
- <RadioList label="Preference" value="" onChange={() => {}}>
434
- <RadioListItem label="Option A" value="a" />
435
- <RadioListItem label="Option B" value="b" />
436
- <RadioListItem label="Option C" value="c" />
437
- </RadioList>
438
- </>,
439
- );
440
- const radios = screen.getAllByRole('radio');
441
- const outside = screen.getByText('before');
442
- outside.focus();
443
- // Forward entry: the browser lands on a leading radio; the group keeps the
444
- // first radio as the deterministic tab stop.
445
- radios[0].focus();
446
- expect(radios[0]).toHaveFocus();
447
-
448
- // Landing on a middle radio from outside is normalized to the first.
449
- outside.focus();
450
- radios[1].focus();
451
- expect(radios[0]).toHaveFocus();
452
- });
453
-
454
342
  it('redirects to the last radio when focus enters an unselected group backward', () => {
455
343
  render(
456
344
  <>
@@ -570,21 +458,6 @@ describe('RadioList', () => {
570
458
  expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
571
459
  });
572
460
 
573
- it('keeps radios focusable via aria-disabled when a reason is provided', () => {
574
- renderGroup();
575
- for (const radio of screen.getAllByRole('radio', h)) {
576
- expect(radio).not.toBeDisabled();
577
- expect(radio).toHaveAttribute('aria-disabled', 'true');
578
- }
579
- });
580
-
581
- it('links the reason tooltip from the group via aria-describedby', () => {
582
- renderGroup();
583
- const group = screen.getByRole('radiogroup');
584
- const tooltip = screen.getByRole('tooltip', h);
585
- expect(group.getAttribute('aria-describedby')).toContain(tooltip.id);
586
- });
587
-
588
461
  it('blocks selection while focusable-disabled', () => {
589
462
  const onChange = vi.fn();
590
463
  renderGroup({onChange});
@@ -592,18 +465,6 @@ describe('RadioList', () => {
592
465
  fireEvent.click(pro);
593
466
  expect(onChange).not.toHaveBeenCalled();
594
467
  });
595
-
596
- it('keeps radios natively disabled when disabled without a reason', () => {
597
- render(
598
- <RadioList label="Plan" value="free" onChange={() => {}} isDisabled>
599
- <RadioListItem label="Free" value="free" />
600
- <RadioListItem label="Pro" value="pro" />
601
- </RadioList>,
602
- );
603
- for (const radio of screen.getAllByRole('radio', h)) {
604
- expect(radio).toBeDisabled();
605
- }
606
- });
607
468
  });
608
469
  describe('form participation', () => {
609
470
  it('submits the selected value under htmlName', () => {