@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.
- package/CHANGELOG.md +41 -3
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
- package/dist/Center/Center.d.ts +23 -16
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +7 -5
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +12 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +1 -0
- package/dist/Field/InputClearButton.d.ts +2 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +5 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +16 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +12 -1
- package/dist/HoverCard/useHoverCard.js +2 -2
- package/dist/Indicator/CheckboxIndicator.js +2 -2
- package/dist/Indicator/RadioIndicator.js +2 -2
- package/dist/Layer/layerStack.d.ts +10 -0
- package/dist/Layer/layerStack.d.ts.map +1 -1
- package/dist/Layer/layerStack.js +21 -9
- package/dist/Layer/useLayerDismissal.d.ts +2 -3
- package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
- package/dist/Layer/useLayerDismissal.js +2 -3
- package/dist/NavIcon/NavIcon.js +2 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +12 -2
- package/dist/Popover/usePopover.d.ts +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +4 -2
- package/dist/ProgressBar/ProgressBar.js +2 -2
- package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
- package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
- package/dist/ScrollableArea/ScrollableArea.js +144 -0
- package/dist/ScrollableArea/index.d.ts +11 -0
- package/dist/ScrollableArea/index.d.ts.map +1 -0
- package/dist/ScrollableArea/index.js +11 -0
- package/dist/StatusDot/StatusDot.js +2 -2
- package/dist/TextArea/TextArea.js +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +19 -4
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +12 -2
- package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +56 -20
- package/dist/astryx.css +15 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scrollGeometry.d.ts +24 -0
- package/dist/hooks/scrollGeometry.d.ts.map +1 -0
- package/dist/hooks/scrollGeometry.js +86 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
- package/dist/hooks/scrollOwnerRegistry.js +24 -0
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +22 -11
- package/dist/hooks/useScrollableArea.d.ts +51 -0
- package/dist/hooks/useScrollableArea.d.ts.map +1 -0
- package/dist/hooks/useScrollableArea.js +287 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +2 -6
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/derivedVarRegistry.js +1 -1
- package/dist/theme/localTokens.d.ts +8 -11
- package/dist/theme/localTokens.d.ts.map +1 -1
- package/dist/theme/localTokens.js +17 -71
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +4 -4
- package/dist/utils/themeProps.d.ts +10 -10
- package/dist/utils/themeProps.d.ts.map +1 -1
- package/dist/utils/themeProps.js +27 -10
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -2
- package/src/AppShell/AppShell.test.tsx +36 -0
- package/src/AppShell/AppShell.tsx +4 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +3 -1
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
- package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
- package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
- package/src/Center/Center.doc.mjs +32 -28
- package/src/Center/Center.spec.md +225 -0
- package/src/Center/Center.test.tsx +42 -4
- package/src/Center/Center.tsx +24 -17
- package/src/Chat/ChatSystemMessage.test.tsx +2 -9
- package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +4 -4
- package/src/DateInput/DateInput.tsx +15 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
- package/src/DateTimeInput/DateTimeInput.tsx +18 -7
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
- package/src/Field/Field.test.tsx +42 -0
- package/src/Field/Field.tsx +6 -0
- package/src/Field/InputClearButton.test.tsx +35 -1
- package/src/Field/InputClearButton.tsx +7 -3
- package/src/Field/PanelSearchInput.tsx +21 -8
- package/src/FieldStatus/FieldStatus.spec.md +27 -17
- package/src/FieldStatus/FieldStatus.test.tsx +7 -5
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
- package/src/FileInput/FileInput.tsx +10 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +4 -4
- package/src/HoverCard/useHoverCard.tsx +2 -2
- package/src/Indicator/CheckboxIndicator.tsx +2 -2
- package/src/Indicator/Indicator.doc.mjs +2 -2
- package/src/Indicator/Indicator.test.tsx +1 -1
- package/src/Indicator/RadioIndicator.tsx +2 -2
- package/src/Layer/layerStack.ts +20 -9
- package/src/Layer/useLayerDismissal.ts +2 -3
- package/src/MultiSelector/MultiSelector.test.tsx +4 -4
- package/src/NavIcon/NavIcon.doc.mjs +4 -4
- package/src/NavIcon/NavIcon.tsx +2 -2
- package/src/NumberInput/NumberInput.tsx +18 -7
- package/src/Popover/Popover.doc.mjs +10 -10
- package/src/Popover/Popover.spec.md +55 -65
- package/src/Popover/Popover.test.tsx +29 -0
- package/src/Popover/usePopover.doc.mjs +4 -4
- package/src/Popover/usePopover.tsx +7 -4
- package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
- package/src/ProgressBar/ProgressBar.test.tsx +1 -31
- package/src/ProgressBar/ProgressBar.tsx +2 -2
- package/src/RadioList/RadioList.test.tsx +5 -144
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
- package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
- package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
- package/src/ScrollableArea/ScrollableArea.tsx +259 -0
- package/src/ScrollableArea/index.ts +26 -0
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
- package/src/Selector/Selector.test.tsx +4 -4
- package/src/Spinner/Spinner.test.tsx +0 -18
- package/src/StatusDot/StatusDot.doc.mjs +4 -4
- package/src/StatusDot/StatusDot.tsx +2 -2
- package/src/TabList/TabList.test.tsx +5 -9
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
- package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
- package/src/Table/Table.doc.mjs +2 -2
- package/src/TextArea/TextArea.doc.mjs +4 -4
- package/src/TextArea/TextArea.tsx +2 -2
- package/src/TextInput/TextInput.doc.mjs +2 -1
- package/src/TextInput/TextInput.test.tsx +94 -0
- package/src/TextInput/TextInput.tsx +22 -6
- package/src/TimeInput/TimeInput.tsx +18 -7
- package/src/Toast/ToastViewport.test.tsx +1 -39
- package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
- package/src/Typeahead/BaseTypeahead.spec.md +269 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
- package/src/Typeahead/BaseTypeahead.tsx +99 -30
- package/src/hooks/index.ts +13 -0
- package/src/hooks/scrollGeometry.ts +155 -0
- package/src/hooks/scrollOwnerRegistry.ts +47 -0
- package/src/hooks/useFocusTrap.ts +22 -11
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
- package/src/hooks/useScrollableArea.doc.mjs +108 -0
- package/src/hooks/useScrollableArea.test.tsx +437 -0
- package/src/hooks/useScrollableArea.ts +469 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +65 -105
- package/src/theme/defineTheme.ts +3 -9
- package/src/theme/derivedVarRegistry.ts +1 -1
- package/src/theme/localTokens.ts +25 -96
- package/src/theme/publicThemeHelperContract.test.ts +2 -2
- package/src/theme/themeAdaptations.test.ts +16 -42
- package/src/theme/themeAdaptations.ts +6 -9
- package/src/utils/themeProps.test.ts +29 -10
- 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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
59
|
-
runtime, DOM, styling,
|
|
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
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
- Migration decision:
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
123
|
-
| Visibility ownership | Uncontrolled or externally controlled | Chooses whether Popover stores visibility or synchronizes to caller state | Popover | Uncontrolled | `component:Popover` | Current public behavior
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
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`
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Every public `usePopover` painted surface belongs to the broad `popover` target
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`popover-surface` themes remain supported
|
|
313
|
-
|
|
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`
|
|
372
|
-
second anatomy part or equal owner. Existing themes using it remain
|
|
373
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
244
|
-
*
|
|
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
|
|
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
|
-
//
|
|
136
|
-
//
|
|
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
|
-
//
|
|
301
|
-
//
|
|
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('
|
|
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;
|
|
486
|
-
//
|
|
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
|
|
7
|
-
*
|
|
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', () => {
|