@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e4f8e4e

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 (146) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +4 -3
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Carousel/Carousel.d.ts.map +1 -1
  13. package/dist/Carousel/Carousel.js +2 -2
  14. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  15. package/dist/Chat/ChatToolCalls.js +34 -17
  16. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +149 -26
  19. package/dist/ContextMenu/index.d.ts +1 -0
  20. package/dist/ContextMenu/index.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  22. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  23. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  24. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  31. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  37. package/dist/DropdownMenu/index.d.ts +2 -1
  38. package/dist/DropdownMenu/index.d.ts.map +1 -1
  39. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  40. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  41. package/dist/DropdownMenu/menuWidth.js +35 -0
  42. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  44. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  45. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  46. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  47. package/dist/MoreMenu/MoreMenu.js +2 -0
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +97 -68
  51. package/dist/MultiSelector/index.d.ts +1 -1
  52. package/dist/MultiSelector/index.d.ts.map +1 -1
  53. package/dist/Popover/Popover.d.ts +10 -6
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +141 -16
  56. package/dist/Popover/usePopover.d.ts +4 -0
  57. package/dist/Popover/usePopover.d.ts.map +1 -1
  58. package/dist/Popover/usePopover.js +52 -5
  59. package/dist/Selector/Selector.d.ts +10 -0
  60. package/dist/Selector/Selector.d.ts.map +1 -1
  61. package/dist/Selector/Selector.js +106 -80
  62. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  63. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  64. package/dist/Selector/SelectorBottomSheet.js +83 -0
  65. package/dist/Selector/index.d.ts +1 -1
  66. package/dist/Selector/index.d.ts.map +1 -1
  67. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  68. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  69. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  70. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  72. package/dist/Selector/useSelectorPresentation.js +90 -0
  73. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  74. package/dist/Toast/useToastGesture.js +46 -9
  75. package/dist/astryx.css +19 -1
  76. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  77. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  78. package/dist/hooks/useAdaptivePresentation.js +19 -0
  79. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  81. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  82. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  83. package/dist/hooks/useFocusTrap.js +12 -0
  84. package/locales/en.json +20 -0
  85. package/locales/pseudo.json +15 -0
  86. package/package.json +2 -2
  87. package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
  88. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  89. package/src/BottomSheet/BottomSheet.tsx +7 -4
  90. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  91. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  92. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  93. package/src/Button/Button.doc.mjs +56 -0
  94. package/src/Button/Button.test.tsx +10 -0
  95. package/src/Button/Button.tsx +13 -5
  96. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  97. package/src/Carousel/Carousel.test.tsx +83 -0
  98. package/src/Carousel/Carousel.tsx +8 -2
  99. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  100. package/src/Chat/ChatToolCalls.tsx +36 -15
  101. package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
  102. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  103. package/src/ContextMenu/ContextMenu.tsx +199 -32
  104. package/src/ContextMenu/index.ts +1 -0
  105. package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
  106. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  107. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  108. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  109. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  110. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  111. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  112. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  113. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  114. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  115. package/src/DropdownMenu/index.ts +2 -0
  116. package/src/DropdownMenu/menuWidth.ts +51 -0
  117. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  118. package/src/IconButton/IconButton.doc.mjs +38 -0
  119. package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
  120. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  121. package/src/MoreMenu/MoreMenu.tsx +13 -1
  122. package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
  123. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  124. package/src/MultiSelector/MultiSelector.tsx +121 -73
  125. package/src/MultiSelector/index.ts +1 -0
  126. package/src/Popover/Popover.doc.mjs +11 -11
  127. package/src/Popover/Popover.test.tsx +397 -2
  128. package/src/Popover/Popover.tsx +228 -21
  129. package/src/Popover/usePopover.tsx +92 -15
  130. package/src/ProgressBar/ProgressBar.spec.md +208 -0
  131. package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
  132. package/src/Selector/Selector.doc.mjs +12 -0
  133. package/src/Selector/Selector.test.tsx +135 -0
  134. package/src/Selector/Selector.tsx +141 -82
  135. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  136. package/src/Selector/index.ts +1 -0
  137. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  138. package/src/Selector/useSelectorPresentation.ts +147 -0
  139. package/src/Toast/ToastViewport.test.tsx +118 -4
  140. package/src/Toast/useToastGesture.ts +61 -8
  141. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  142. package/src/hooks/useAdaptivePresentation.ts +35 -0
  143. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  144. package/src/hooks/useFocusTrap.test.tsx +40 -0
  145. package/src/hooks/useFocusTrap.ts +13 -0
  146. package/src/theme/themingTargets.test.ts +91 -25
@@ -0,0 +1,208 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 1
4
+ kind: component
5
+ id: component:ProgressBar
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [public-api, behavior, theming, accessibility]
13
+ verified_by: [packages/core/src/ProgressBar/ProgressBar.test.tsx]
14
+ families: []
15
+ design_specs: []
16
+ architecture:
17
+ [
18
+ architecture:public-component-api,
19
+ architecture:component-theming-surface,
20
+ architecture:theme-authoring-contract,
21
+ architecture:theme-tokens,
22
+ ]
23
+ contributing: []
24
+ system_specs: [spec:AST-002/DEC-1, spec:AST-002/DEC-2]
25
+ ---
26
+
27
+ # ProgressBar component contract
28
+
29
+ ## Intent
30
+
31
+ ProgressBar communicates the progress of an operation. A determinate bar must be
32
+ a sufficient standalone visual: people must be able to distinguish completed
33
+ from remaining progress without depending on visible text rendered elsewhere.
34
+ External text may supplement the bar, but it is not a prerequisite for the bar
35
+ to be correct.
36
+
37
+ This draft is intentionally limited to that visual-completeness requirement and
38
+ the resulting public-API boundary. It does not choose a visual treatment.
39
+
40
+ ## Compatibility and migration
41
+
42
+ - Released default preserved: `yes`; this draft changes no runtime behavior.
43
+ - Compatibility class: documentation-only draft; no public API is added, removed,
44
+ or changed.
45
+ - Controlled/uncontrolled behavior: unchanged.
46
+ - Migration decision: `component:ProgressBar/DEC-1`.
47
+
48
+ Consumer migration instructions belong in consumer docs and release notes.
49
+
50
+ ## Ownership boundary
51
+
52
+ **Owns**
53
+
54
+ - A sufficient standalone visual distinction between completed and remaining
55
+ progress for every determinate presentation the component provides.
56
+ - Internal resolution of that visual treatment when the caller supplies ordinary
57
+ progress state.
58
+
59
+ **Does not own / non-goals**
60
+
61
+ - External visible labels, values, or descriptions — owned by the product
62
+ callsite and supplementary to the bar.
63
+ - The exact standalone contrast treatment — still a human design decision.
64
+ - Theme token definitions — owned by `architecture:theme-tokens`.
65
+ - Public API that makes visual correctness depend on caller-declared external
66
+ content — governed by `architecture:public-component-api` and `spec:AST-002`.
67
+
68
+ ## Public concepts
69
+
70
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
71
+ | ------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------- | ------------------------------- | ----------------------- | ------------------ | ------------------------------ |
72
+ | Progress mode | `determinate`, `indeterminate` | Whether completed progress is known | All variants | Determinate | `component:ProgressBar` | Stable current API | Existing prop handling applies |
73
+ | Built-in value text | Shown or hidden | Optional visible text supplement generated by ProgressBar | Determinate only | Hidden | `component:ProgressBar` | Stable current API | Ignored when indeterminate |
74
+ | Visual treatment | One sufficient standalone treatment; exact form unresolved | How the graphic distinguishes completed from remaining progress | Determinate progress | Sufficient standalone treatment | `component:ProgressBar` | Draft decision | No incomplete public mode |
75
+
76
+ The current public props remain documented in `ProgressBar.doc.mjs`. This draft
77
+ does not turn nearby external content into a ProgressBar concept.
78
+
79
+ ## Behavioral and layout contract
80
+
81
+ Draft requirements identify their basis so observed code is not mistaken for an
82
+ intentional decision.
83
+
84
+ | ID | Candidate invariant | Basis | Draft review state |
85
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------- |
86
+ | FR1 | Every determinate presentation ProgressBar provides MUST be a sufficient standalone visual for distinguishing completed from remaining progress. | `component:ProgressBar/DEC-1`; existing non-text contrast standard | Settled |
87
+ | FR2 | External visible text MAY supplement the bar but MUST NOT be required for the bar's visual correctness. | `component:ProgressBar/DEC-1` | Settled |
88
+ | FR3 | ProgressBar MUST NOT make visual correctness depend on caller-declared external content that it cannot verify or associate. | `component:ProgressBar/DEC-1`; `spec:AST-002` | Settled |
89
+ | FR4 | Supplementary text or graphics MUST NOT substitute for a sufficient distinction between completed and remaining progress. | `component:ProgressBar/DEC-1` | Settled |
90
+ | FR5 | Public API MAY be considered only after the component has a correct base behavior and a stable caller-owned distinction still remains. | `component:ProgressBar/DEC-1`; `spec:AST-002` | Settled admission gate |
91
+
92
+ ### Observed current behavior
93
+
94
+ These observations describe `main`; they are not design approval from this
95
+ draft:
96
+
97
+ - Determinate progress renders a semantic-color fill over a muted track.
98
+ - `hasValueLabel` optionally renders formatted value text in the component.
99
+ - Callers may compose other visible text outside the component.
100
+ - The current public theming surface exposes root, fill, track, and mark targets;
101
+ variant state is reflected on the root, fill, and mark targets.
102
+ - Current unit tests cover value semantics, labels, variants, determinate and
103
+ indeterminate modes, disabled rendering, marks, and public target names. They
104
+ do not establish a sufficient standalone completed-versus-remaining visual
105
+ across shipped themes.
106
+
107
+ ### Allowed variation
108
+
109
+ - **AV1 — Exact visual treatment.** Shape, token choice, and other rendering
110
+ details may vary if every resulting presentation satisfies FR1.
111
+ - **AV2 — Themes.** Themes may express their visual language through the current
112
+ theming system while preserving the standalone distinction.
113
+
114
+ ### Representative states
115
+
116
+ | State | Required invariant | Allowed variation |
117
+ | -------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
118
+ | Determinate, partial progress | Completed and remaining progress are distinguishable from the graphic alone | Exact visual treatment and semantic variant |
119
+ | Determinate with built-in value text | The graphic still satisfies FR1; value text supplements it | Value formatting |
120
+ | Determinate with external visible text | The graphic still satisfies FR1 without relying on that text | Callsite composition |
121
+ | Indeterminate | Communicates ongoing activity without claiming a completed amount | Existing animation and theme variation; completed-versus-remaining distinction does not apply |
122
+
123
+ ### Transformation and precedence order
124
+
125
+ - **ORD1 — Progress state before presentation.** Resolve determinate or
126
+ indeterminate behavior from component state, then render a treatment valid for
127
+ that mode. External content does not weaken the determinate visual requirement.
128
+
129
+ ### Performance and resources
130
+
131
+ - **PR1 — No external-content inspection.** ProgressBar MUST NOT add DOM
132
+ observation, measurement, or relationship discovery to decide whether nearby
133
+ visible text makes a weaker bar acceptable.
134
+
135
+ ## Accessibility contract
136
+
137
+ - **AR1 — Standalone non-text distinction.** A determinate bar MUST satisfy the
138
+ applicable non-text contrast requirement without relying on external visible
139
+ text.
140
+ - **AR2 — Programmatic semantics remain present.** The current required
141
+ accessible name and determinate value semantics remain independent of the
142
+ visible treatment.
143
+ - **AR3 — Supplementary text is not inferred.** ProgressBar MUST NOT claim that
144
+ external text is present, visible, equivalent, or correctly associated when it
145
+ cannot encode or verify those facts.
146
+
147
+ ## Design relationships
148
+
149
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
150
+ | ------------------------------------ | ------------------------------------------- | ------------------------ | -------------- | ------------------ |
151
+ | Determinate fill and remaining track | Sufficient standalone distinction | Unsettled | Prominent | FR1, AR1 |
152
+ | Built-in or external visible value | Supplements rather than enables correctness | Prescribed | Supporting | FR2, AR3 |
153
+
154
+ The component implements design requirements without copying their rationale.
155
+ The standalone visual remains a human design decision; this contract does not
156
+ invent its form.
157
+
158
+ ## Family and system relationships
159
+
160
+ - `architecture:public-component-api` and `spec:AST-002/DEC-1` govern whether a
161
+ caller-owned distinction justifies a public prop.
162
+ - `architecture:component-theming-surface` governs which stable visible parts and
163
+ states become public theme capabilities.
164
+ - `architecture:theme-authoring-contract` governs how themes override those
165
+ component capabilities without creating a second component contract.
166
+ - `architecture:theme-tokens` governs the semantic token vocabulary used by the
167
+ eventual treatment.
168
+
169
+ ## Verification map
170
+
171
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
172
+ | ------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------- |
173
+ | FR1, AR1 | Real-browser visual and contrast evidence across shipped themes and color modes | Partial determinate progress for each semantic variant | A completed or remaining segment becomes indistinguishable without text | Future ProgressBar visual audit |
174
+ | FR2, FR3, AR3 | Public type and consumer-doc review | No value text, built-in value text, external composed text | A caller signal is credited with correctness the component cannot verify | Future ProgressBar API tests |
175
+ | AR2 | `ProgressBar.test.tsx` | Determinate, indeterminate, hidden label, custom value text | Accessible name or value semantics disappear when visuals change | Future ProgressBar accessibility audit |
176
+ | Theming | Theme-target metadata checks plus browser evidence | Shipped themes, light and dark modes | A theme override bypasses the standalone distinction | Future ProgressBar theming audit |
177
+
178
+ ## Decision log
179
+
180
+ ### DEC-1 — ProgressBar owns a sufficient standalone visual
181
+
182
+ **Reference:** `component:ProgressBar/DEC-1`
183
+ **Decider:** `cixzhang`, `2026-08-30`
184
+
185
+ ProgressBar must provide a sufficient standalone visual. Visible text inside or
186
+ outside the component may improve understanding, but the bar remains responsible
187
+ for communicating completed versus remaining progress without it.
188
+
189
+ Public API must not make that correctness depend on external content the
190
+ component cannot carry, associate, or verify. Solve the component behavior first.
191
+ Consider new API only when a stable caller-owned distinction still remains after
192
+ the base behavior is correct.
193
+
194
+ Rejected: weakening the bar based on a caller claim about nearby content. That
195
+ would move component correctness into an external condition the component cannot
196
+ verify.
197
+
198
+ ## Open questions
199
+
200
+ - **OQ1 — What exact visual treatment gives every determinate presentation a
201
+ sufficient standalone completed-versus-remaining distinction across supported
202
+ variants, themes, and color modes?** (`human-design`)
203
+
204
+ ## Content boundary
205
+
206
+ This file does not duplicate consumer prop tables or examples, current audit
207
+ results, implementation steps, exact visual values, or system theming rules. It
208
+ links to their owners.
@@ -98,6 +98,62 @@ export const docs = {
98
98
  usage: {
99
99
  description:
100
100
  'A segmented button group that allows users to make a single selection from a small set of mutually exclusive options. Use SegmentedControl when all options should be visible at once and the selection controls a value or mode, not page navigation.',
101
+ accessibility: [
102
+ {
103
+ name: 'Text label',
104
+ category: 'Color contrast',
105
+ criterion: '1.4.3 Contrast (Minimum)',
106
+ requirement: '4.5:1',
107
+ states: ['Rest', 'Hover', 'Selected'],
108
+ description:
109
+ 'Each label must have at least 4.5:1 contrast with its segment background. Check unselected, Hover, and selected colors as they appear on screen.',
110
+ },
111
+ {
112
+ name: 'Essential icon',
113
+ category: 'Color contrast',
114
+ criterion: '1.4.11 Non-text Contrast',
115
+ requirement: '3:1',
116
+ states: ['Icon only'],
117
+ description:
118
+ 'When a segment has no visible label, its icon must have at least 3:1 contrast with the segment background. An icon beside a visible label does not need its own check.',
119
+ },
120
+ {
121
+ name: 'Selected state indicator',
122
+ category: 'Color contrast',
123
+ criterion: '1.4.11 Non-text Contrast',
124
+ requirement: '3:1 if relied upon',
125
+ states: ['Selected'],
126
+ description:
127
+ 'The selected background must reach 3:1 only when users need it to tell selected from unselected. Label color and weight also show selection.',
128
+ },
129
+ {
130
+ name: 'Visible control boundary',
131
+ category: 'Color contrast',
132
+ criterion: '1.4.11 Non-text Contrast',
133
+ requirement: '3:1 if needed',
134
+ states: ['Rest'],
135
+ description:
136
+ 'The control edge or segment borders need at least 3:1 contrast when users need them to see the choices.',
137
+ },
138
+ {
139
+ name: 'Keyboard focus indicator',
140
+ category: 'Color contrast',
141
+ criterion: '1.4.11 Non-text Contrast',
142
+ requirement: '3:1',
143
+ states: ['Focus visible'],
144
+ description:
145
+ 'The focus outline must have at least 3:1 contrast with the area around the segment. Check it on the track and selected background.',
146
+ },
147
+ {
148
+ name: 'Disabled appearance',
149
+ category: 'Color contrast',
150
+ criterion: '1.4.3 and 1.4.11 exceptions',
151
+ requirement: 'Not required',
152
+ states: ['Disabled'],
153
+ description:
154
+ 'Disabled controls do not need to meet these contrast ratios.',
155
+ },
156
+ ],
101
157
  bestPractices: [
102
158
  {guidance: true, description: 'Use for switching between 2–5 mutually exclusive views or modes where all options should be visible.'},
103
159
  {guidance: true, description: 'Provide a descriptive label for the control to ensure the group is accessible to screen readers.'},
@@ -194,6 +194,13 @@ export const docs = {
194
194
  'Which edge of the option row carries the selected mark. start reserves a mark column ahead of every label so they stay aligned, the way a native menu does; end is the house convention shared with Typeahead and CommandPalette.',
195
195
  default: "'end'",
196
196
  },
197
+ {
198
+ name: 'presentation',
199
+ type: "'popover' | 'bottom-sheet' | 'adaptive'",
200
+ description:
201
+ 'How the option list is presented. adaptive uses a bottom sheet on compact touch screens and an anchored popover otherwise.',
202
+ default: "'popover'",
203
+ },
197
204
  {
198
205
  name: 'width',
199
206
  type: 'SizeValue',
@@ -253,6 +260,11 @@ export const docs = {
253
260
  description:
254
261
  'Use variant="ghost" when a selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
255
262
  },
263
+ {
264
+ guidance: true,
265
+ description:
266
+ 'Use presentation="adaptive" when the selector should become a bottom sheet on compact touch screens.',
267
+ },
256
268
  {
257
269
  guidance: false,
258
270
  description:
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
+ import {readFileSync} from 'node:fs';
13
14
  import {
14
15
  act,
15
16
  render,
@@ -19,6 +20,7 @@ import {
19
20
  within,
20
21
  } from '@testing-library/react';
21
22
  import userEvent from '@testing-library/user-event';
23
+ import * as stylex from '@stylexjs/stylex';
22
24
  import {useState} from 'react';
23
25
  import type {ReactNode} from 'react';
24
26
  import {Selector} from './Selector';
@@ -35,6 +37,7 @@ import {defineTheme} from '../theme/defineTheme';
35
37
  import {Theme} from '../theme/Theme';
36
38
  import {generateThemeCSS} from '../theme/generateThemeRules';
37
39
  import {spacingVars} from '../theme/tokens.stylex';
40
+ import {selectorPresentationStyles} from './selectorPresentation.stylex';
38
41
 
39
42
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
40
43
  const {prose, component} = generateThemeCSS(theme);
@@ -45,6 +48,28 @@ function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
45
48
  beforeEach(() => {
46
49
  // The live regions are a document-level singleton; start each test clean.
47
50
  __resetLiveRegionsForTest();
51
+ __resetInteractionModalityForTest();
52
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
53
+ this: HTMLDialogElement,
54
+ ) {
55
+ this.setAttribute('open', '');
56
+ });
57
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
58
+ this.removeAttribute('open');
59
+ });
60
+ vi.stubGlobal(
61
+ 'matchMedia',
62
+ vi.fn().mockImplementation((query: string) => ({
63
+ matches: false,
64
+ media: query,
65
+ onchange: null,
66
+ addEventListener: vi.fn(),
67
+ removeEventListener: vi.fn(),
68
+ addListener: vi.fn(),
69
+ removeListener: vi.fn(),
70
+ dispatchEvent: vi.fn(),
71
+ })),
72
+ );
48
73
  HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
49
74
  this.setAttribute('popover-open', '');
50
75
  const event = new Event('toggle', {bubbles: false});
@@ -228,6 +253,116 @@ function mockSelectorRects({
228
253
  }
229
254
 
230
255
  describe('Selector', () => {
256
+ it('uses a bottom sheet and closes after a selection when requested', async () => {
257
+ const user = userEvent.setup();
258
+ const onChange = vi.fn();
259
+ render(
260
+ <Selector
261
+ label="Fruit"
262
+ options={OPTIONS}
263
+ onChange={onChange}
264
+ presentation="bottom-sheet"
265
+ />,
266
+ );
267
+
268
+ const trigger = screen.getByRole('combobox');
269
+ await user.click(trigger);
270
+
271
+ expect(
272
+ await screen.findByRole('dialog', {name: 'Fruit'}),
273
+ ).toBeInTheDocument();
274
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
275
+
276
+ await user.click(screen.getByRole('option', {name: /Banana/}));
277
+ expect(onChange).toHaveBeenCalledWith('Banana');
278
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
279
+ });
280
+
281
+ it('restores touch focus without painting a trigger focus ring', async () => {
282
+ render(
283
+ <Selector
284
+ label="Fruit"
285
+ options={OPTIONS}
286
+ onChange={() => {}}
287
+ presentation="bottom-sheet"
288
+ />,
289
+ );
290
+
291
+ const trigger = screen.getByRole('combobox');
292
+ fireEvent.pointerDown(trigger, {pointerType: 'touch'});
293
+ fireEvent.click(trigger, {detail: 1});
294
+ const option = await screen.findByRole('option', {name: /Banana/});
295
+ fireEvent.pointerDown(option, {pointerType: 'touch'});
296
+ fireEvent.click(option, {detail: 1});
297
+
298
+ trigger.focus();
299
+ expect(trigger).toHaveFocus();
300
+ expect(trigger.parentElement).toHaveClass(
301
+ stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
302
+ );
303
+ });
304
+
305
+ it('uses content-hugging height without extra bottom content padding', () => {
306
+ const source = readFileSync(
307
+ 'packages/core/src/Selector/SelectorBottomSheet.tsx',
308
+ 'utf8',
309
+ );
310
+
311
+ expect(source).toContain('height="hug"');
312
+ expect(source).toContain('paddingBlockStart={4}');
313
+ expect(source).toContain('paddingBlockEnd={0}');
314
+ });
315
+
316
+ it('moves keyboard focus into a bottom-sheet listbox', async () => {
317
+ const user = userEvent.setup();
318
+ render(
319
+ <Selector label="Fruit" options={OPTIONS} presentation="bottom-sheet" />,
320
+ );
321
+
322
+ const trigger = screen.getByRole('combobox');
323
+ trigger.focus();
324
+ await user.keyboard('{Enter}');
325
+
326
+ await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
327
+ });
328
+
329
+ it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
330
+ vi.stubGlobal(
331
+ 'matchMedia',
332
+ vi.fn().mockImplementation((query: string) => ({
333
+ matches: query === '(max-width: 768px) and (pointer: coarse)',
334
+ media: query,
335
+ onchange: null,
336
+ addEventListener: vi.fn(),
337
+ removeEventListener: vi.fn(),
338
+ addListener: vi.fn(),
339
+ removeListener: vi.fn(),
340
+ dispatchEvent: vi.fn(),
341
+ })),
342
+ );
343
+ const user = userEvent.setup();
344
+ render(
345
+ <Selector label="Fruit" options={OPTIONS} presentation="adaptive" />,
346
+ );
347
+
348
+ await user.click(screen.getByRole('combobox'));
349
+ expect(
350
+ await screen.findByRole('dialog', {name: 'Fruit'}),
351
+ ).toBeInTheDocument();
352
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
353
+ });
354
+
355
+ it('keeps adaptive presentation anchored without compact touch', async () => {
356
+ const user = userEvent.setup();
357
+ render(
358
+ <Selector label="Fruit" options={OPTIONS} presentation="adaptive" />,
359
+ );
360
+
361
+ await user.click(screen.getByRole('combobox'));
362
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalledOnce();
363
+ expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled();
364
+ });
365
+
231
366
  it('renders with placeholder when no value', () => {
232
367
  render(<Selector label="Fruit" options={OPTIONS} placeholder="Pick one" />);
233
368
  expect(screen.getByRole('combobox')).toHaveTextContent('Pick one');