@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.
- package/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +19 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/locales/en.json +20 -0
- package/locales/pseudo.json +15 -0
- package/package.json +2 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/ProgressBar/ProgressBar.spec.md +208 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
- package/src/Selector/Selector.doc.mjs +12 -0
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- 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');
|