@astryxdesign/core 0.6.1 → 0.6.2-canary.176dfa6
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 +33 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +16 -9
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -7
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +9 -3
- package/dist/Markdown/Markdown.d.ts +10 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +58 -14
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +126 -12
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +369 -34
- package/dist/Markdown/utils.d.ts +1 -1
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +5 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -15
- package/dist/ToggleButton/ToggleButton.d.ts +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +15 -6
- package/dist/astryx.css +5 -2
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -5
- package/scripts/agent-doc-state.mjs +1 -1
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
- package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
- package/src/DateRangeInput/DateRangeInput.tsx +29 -20
- package/src/Dialog/Dialog.doc.mjs +3 -0
- package/src/Dialog/Dialog.spec.md +1 -1
- package/src/Dialog/DialogHeader.doc.mjs +38 -0
- package/src/Dialog/DialogHeader.test.tsx +49 -0
- package/src/Dialog/DialogHeader.tsx +23 -4
- package/src/Dialog/modules/DialogHeader.spec.md +152 -0
- package/src/FileInput/FileInput.doc.mjs +2 -0
- package/src/FileInput/FileInput.spec.md +199 -0
- package/src/FileInput/FileInput.test.tsx +14 -0
- package/src/FileInput/FileInput.tsx +13 -3
- package/src/Markdown/Markdown.doc.mjs +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +149 -70
- package/src/Markdown/Markdown.test.tsx +107 -3
- package/src/Markdown/Markdown.tsx +116 -35
- package/src/Markdown/incremental.test.ts +175 -7
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +609 -81
- package/src/Markdown/utils.ts +6 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/Slider/Slider.doc.mjs +16 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +18 -0
- package/src/Slider/Slider.tsx +12 -6
- package/src/Spinner/Spinner.doc.mjs +6 -3
- package/src/Spinner/Spinner.test.tsx +37 -0
- package/src/Spinner/Spinner.tsx +31 -14
- package/src/ToggleButton/ToggleButton.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +15 -6
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/theme/derivedVarRegistry.test.ts +6 -4
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
schema_version: 3
|
|
3
|
-
template_version:
|
|
3
|
+
template_version: 4
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:Slider
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-09-14
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [theming]
|
|
13
13
|
verified_by:
|
|
@@ -29,14 +29,14 @@ system_specs: []
|
|
|
29
29
|
## Intent
|
|
30
30
|
|
|
31
31
|
Slider presents a labeled control for selecting one numeric value or a bounded
|
|
32
|
-
range. This
|
|
33
|
-
|
|
32
|
+
range. This contract records its consumer anatomy and theming ownership, including
|
|
33
|
+
an additive target for the interactive control surface.
|
|
34
34
|
|
|
35
35
|
## Compatibility and migration
|
|
36
36
|
|
|
37
37
|
- Released default preserved: `yes`
|
|
38
|
-
- Compatibility class: additive
|
|
39
|
-
|
|
38
|
+
- Compatibility class: additive target and state reflection only; runtime,
|
|
39
|
+
default styling, DOM semantics, and public props remain unchanged
|
|
40
40
|
- Controlled/uncontrolled behavior: unchanged; Slider remains controlled
|
|
41
41
|
- Migration decision: none
|
|
42
42
|
|
|
@@ -48,7 +48,8 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
48
48
|
|
|
49
49
|
- The current slider row, background track, filled range, tick marks and labels,
|
|
50
50
|
thumbs, and adjacent text value presentation.
|
|
51
|
-
- The
|
|
51
|
+
- The `slider`, `slider-control`, `slider-track`, and `slider-thumb` public
|
|
52
|
+
targets.
|
|
52
53
|
|
|
53
54
|
**Does not own / non-goals**
|
|
54
55
|
|
|
@@ -61,24 +62,26 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
61
62
|
|
|
62
63
|
## Public concepts
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
This adds one public theming target without adding or changing a component prop,
|
|
66
|
+
value domain, or behavior. Consumer props, modes, states, and usage remain
|
|
67
|
+
documented in `Slider.doc.mjs`.
|
|
66
68
|
|
|
67
69
|
## Behavioral and layout contract
|
|
68
70
|
|
|
69
|
-
| ID | Candidate invariant
|
|
70
|
-
| --- |
|
|
71
|
-
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track.
|
|
72
|
-
| FR2 | `Slider`, `Track`, and `Thumb` carry the
|
|
73
|
-
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target.
|
|
74
|
-
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip.
|
|
71
|
+
| ID | Candidate invariant | Basis | Review state |
|
|
72
|
+
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
73
|
+
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided |
|
|
74
|
+
| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and #6224 | Approved additive target contract |
|
|
75
|
+
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
|
|
76
|
+
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
|
|
75
77
|
|
|
76
78
|
### Observed current target asymmetry
|
|
77
79
|
|
|
78
80
|
ProgressBar currently exposes targets for its fill and marks, while Slider
|
|
79
|
-
exposes targets for its root row, background track, and
|
|
80
|
-
filled range, tick marks, mark labels, or adjacent text value
|
|
81
|
-
implementation evidence for a joint audit, not approval of
|
|
81
|
+
exposes targets for its root row, interactive control, background track, and
|
|
82
|
+
thumbs but not its filled range, tick marks, mark labels, or adjacent text value
|
|
83
|
+
display. This is implementation evidence for a joint audit, not approval of
|
|
84
|
+
either component's remaining target shape.
|
|
82
85
|
|
|
83
86
|
### Allowed variation
|
|
84
87
|
|
|
@@ -108,20 +111,21 @@ implementation evidence for a joint audit, not approval of either target shape.
|
|
|
108
111
|
|
|
109
112
|
## Accessibility contract
|
|
110
113
|
|
|
111
|
-
This
|
|
114
|
+
This contract does not change or extend Slider's existing accessible name, value,
|
|
112
115
|
range-thumb naming, description/status association, keyboard behavior, disabled
|
|
113
116
|
behavior, or value-tooltip behavior.
|
|
114
117
|
|
|
115
118
|
## Design relationships
|
|
116
119
|
|
|
117
|
-
| Anatomy or state
|
|
118
|
-
|
|
|
119
|
-
| Label and description
|
|
120
|
-
| Slider and
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
120
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
121
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------- | -------------- | ------------------ |
|
|
122
|
+
| Label and description | Identify and explain the numeric setting. | Current shared-component source | Supporting | FR4 |
|
|
123
|
+
| Slider and interactive control | Separate the outer row from the pointer/keyboard surface and its composite disabled treatment. | Current source and public docs | Prominent | FR1, FR2 |
|
|
124
|
+
| Track | Shows the available range behind the fill. | Current source and public docs | Prominent | FR1, FR2 |
|
|
125
|
+
| Filled range and thumbs | Show the selected value or interval over the available range. | Current source and public docs | Prominent | FR1, FR2, FR3 |
|
|
126
|
+
| Tick marks and labels | Show optional supplied positions and their text. | Current source and public docs | Supporting | FR1, FR3 |
|
|
127
|
+
| Value presentation | Shows the formatted value as text or a shared Tooltip. | Current source and public docs | Supporting | FR3, FR4 |
|
|
128
|
+
| Status message | Presents shared validation feedback below the slider. | Current shared-component source | Supporting | FR4 |
|
|
125
129
|
|
|
126
130
|
### Theming anatomy
|
|
127
131
|
|
|
@@ -138,6 +142,7 @@ behavior, or value-tooltip behavior.
|
|
|
138
142
|
}
|
|
139
143
|
},
|
|
140
144
|
"Slider": {"target": "slider"},
|
|
145
|
+
"Interactive control": {"target": "slider-control"},
|
|
141
146
|
"Track": {"target": "slider-track"},
|
|
142
147
|
"Filled range": {
|
|
143
148
|
"none": {
|
|
@@ -172,10 +177,12 @@ behavior, or value-tooltip behavior.
|
|
|
172
177
|
}
|
|
173
178
|
```
|
|
174
179
|
|
|
175
|
-
`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
`Interactive control` names the pointer/keyboard hit surface and the composite
|
|
181
|
+
opacity boundary around the rail, fill, marks, and thumbs. `Filled range`, `Tick
|
|
182
|
+
mark`, `Mark label`, and `Value display` remain stable consumer anatomy, but no
|
|
183
|
+
current Slider target reaches them. The map records those gaps without making
|
|
184
|
+
their absence intentional. `Value display` names the adjacent text mode; the
|
|
185
|
+
separately listed value tooltip retains Tooltip's target.
|
|
179
186
|
|
|
180
187
|
## Family and system relationships
|
|
181
188
|
|
|
@@ -191,24 +198,31 @@ adjacent text mode; the separately listed value tooltip retains Tooltip's target
|
|
|
191
198
|
|
|
192
199
|
## Verification map
|
|
193
200
|
|
|
194
|
-
| Contract | Verification | Representative states
|
|
195
|
-
| ------------------- | -------------------------------------------------------------------------------------- |
|
|
196
|
-
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks
|
|
197
|
-
| FR2 | `themingTargets.test.ts
|
|
198
|
-
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value
|
|
199
|
-
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason
|
|
200
|
-
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and
|
|
201
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
202
|
+
| ------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------- |
|
|
203
|
+
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
|
|
204
|
+
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
|
|
205
|
+
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
|
|
206
|
+
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
|
|
207
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
|
|
201
208
|
|
|
202
|
-
The focused Slider suite
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
The focused Slider suite asserts the `slider-control` target's orientation and
|
|
210
|
+
disabled state. Source/metadata guards cover all four target declarations. The
|
|
211
|
+
suite covers only the disabled-reason Tooltip path, not the
|
|
212
|
+
`valueDisplay="tooltip"` composition; that anatomy is source-inspected and
|
|
213
|
+
remains missing focused test coverage.
|
|
207
214
|
|
|
208
215
|
## Decision log
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
### DEC-1 — Interactive control is public Slider anatomy
|
|
218
|
+
|
|
219
|
+
**Reference:** `component:Slider/DEC-1`
|
|
220
|
+
**Decider:** cixzhang, 2026-09-14
|
|
221
|
+
|
|
222
|
+
The stable pointer/keyboard interaction and compositing surface is a
|
|
223
|
+
consumer-recognizable part, so it carries `slider-control`. `Thumb` remains
|
|
224
|
+
separate anatomy and continues to carry `slider-thumb`. This adds no default
|
|
225
|
+
behavior or styling change.
|
|
212
226
|
|
|
213
227
|
## Open questions
|
|
214
228
|
|
|
@@ -137,6 +137,24 @@ describe('Slider', () => {
|
|
|
137
137
|
},
|
|
138
138
|
);
|
|
139
139
|
|
|
140
|
+
it('reflects orientation and disabled state on the interactive control target', () => {
|
|
141
|
+
render(
|
|
142
|
+
<Slider
|
|
143
|
+
label="Volume"
|
|
144
|
+
value={50}
|
|
145
|
+
orientation="vertical"
|
|
146
|
+
valueDisplay="text"
|
|
147
|
+
isDisabled
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
const control = screen.getByRole('slider').parentElement;
|
|
151
|
+
expect(control).not.toBeNull();
|
|
152
|
+
expect(control).toHaveClass('astryx-slider-control');
|
|
153
|
+
expect(control).toHaveAttribute('data-orientation', 'vertical');
|
|
154
|
+
expect(control).toHaveAttribute('data-disabled', 'disabled');
|
|
155
|
+
expect(control).not.toContainElement(screen.getByText('50'));
|
|
156
|
+
});
|
|
157
|
+
|
|
140
158
|
it('range mode sets correct aria values on both thumbs', () => {
|
|
141
159
|
render(
|
|
142
160
|
<Slider
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -1029,12 +1029,18 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1029
1029
|
onPointerMove={handlePointerMove}
|
|
1030
1030
|
onPointerUp={handlePointerUp}
|
|
1031
1031
|
onPointerCancel={handlePointerUp}
|
|
1032
|
-
{...
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
?
|
|
1036
|
-
|
|
1037
|
-
|
|
1032
|
+
{...mergeProps(
|
|
1033
|
+
themeProps('slider-control', {
|
|
1034
|
+
orientation,
|
|
1035
|
+
disabled: isDisabled ? 'disabled' : null,
|
|
1036
|
+
}),
|
|
1037
|
+
stylex.props(
|
|
1038
|
+
styles.trackContainer,
|
|
1039
|
+
isHorizontal
|
|
1040
|
+
? styles.trackContainerHorizontal
|
|
1041
|
+
: styles.trackContainerVertical,
|
|
1042
|
+
isDisabled && styles.trackContainerDisabled,
|
|
1043
|
+
),
|
|
1038
1044
|
)}>
|
|
1039
1045
|
{/* Background track */}
|
|
1040
1046
|
<div
|
|
@@ -30,7 +30,7 @@ export const docs = {
|
|
|
30
30
|
name: 'aria-label',
|
|
31
31
|
type: 'string',
|
|
32
32
|
description:
|
|
33
|
-
'Accessible name for screen readers. Defaults to label (if string) or "Loading".',
|
|
33
|
+
'Accessible name for screen readers. Defaults to label (if string) or a translated "Loading" from the i18n catalog.',
|
|
34
34
|
default: "'Loading'",
|
|
35
35
|
},
|
|
36
36
|
{
|
|
@@ -48,6 +48,7 @@ export const docs = {
|
|
|
48
48
|
{name: '--spinner-stroke-width', description: 'Stroke width of both circles the ring is drawn from: the moving arc and the track behind it. Set it per size alongside the diameter. One stroke width drives both, so 0 is honoured as a zero-width stroke and paints nothing at all rather than falling back to the default; for an arc with no track behind it, set --spinner-track-color to transparent instead.', default: '2px (sm), 3px (md), 3px (lg), 4px (xl)'},
|
|
49
49
|
{name: '--spinner-color', description: "Color of the moving arc. Defaults to the shade's token, so set it on a shade-variant target to retheme one shade (spinner: { 'shade:subtle': { '--spinner-color': 'var(--color-text-tertiary)' } }), or on the base target to retheme all four. Accepts any color notation, including var(), color-mix() and currentColor.", default: 'var(--color-accent) (default), var(--color-text-secondary) (subtle), var(--color-on-dark) (onMedia), currentColor (inherit)'},
|
|
50
50
|
{name: '--spinner-track-color', description: 'Color of the track the arc travels on. Set it to `transparent` for an arc with no track. The onMedia and inherit shades draw the track at reduced alpha (30%) so it reads against an arbitrary backdrop; that fade applies to a themed color too.', default: 'var(--color-track) (default, subtle), var(--color-on-dark) (onMedia), currentColor (inherit)'},
|
|
51
|
+
{name: '--spinner-arc-fraction', description: "Fraction of the ring the moving arc covers, as a plain number (not a percentage or angle). Set it on a size-variant target the same way as --spinner-diameter, e.g. spinner: { 'size:xl': { '--spinner-arc-fraction': '0.75' } } for a 270deg sweep. Only takes effect once the stylesheet loads; a render with no CSS (SSR, no-JS) always draws the default 135deg arc.", default: '0.375 (135deg), same for every size'},
|
|
51
52
|
],
|
|
52
53
|
},
|
|
53
54
|
usage: {
|
|
@@ -87,7 +88,7 @@ export const docsZh = {
|
|
|
87
88
|
{
|
|
88
89
|
name: 'aria-label',
|
|
89
90
|
type: 'string',
|
|
90
|
-
description: '屏幕阅读器的无障碍名称。默认为 label
|
|
91
|
+
description: '屏幕阅读器的无障碍名称。默认为 label(如果是字符串)或来自 i18n 词条的已翻译 "Loading"。',
|
|
91
92
|
default: "'Loading'",
|
|
92
93
|
},
|
|
93
94
|
{
|
|
@@ -106,6 +107,7 @@ export const docsZh = {
|
|
|
106
107
|
{name: '--spinner-stroke-width', description: '绘制环的两个圆——移动圆弧与其后的轨道——的描边宽度。与直径一起按尺寸设置。同一个描边宽度同时驱动两者,因此 0 会被采纳为零宽描边——什么都不绘制,而不会回退到默认值;若想要没有轨道的圆弧,请改将 --spinner-track-color 设为 transparent。', default: '2px (sm), 3px (md), 3px (lg), 4px (xl)'},
|
|
107
108
|
{name: '--spinner-color', description: "运动圆弧的颜色。默认取所在 shade 的令牌,因此可在 shade 变体目标上设置以重新定义单个 shade——spinner: { 'shade:subtle': { '--spinner-color': 'var(--color-text-tertiary)' } }——或在 base 目标上设置以覆盖全部四种。接受任意颜色写法,包括 var()、color-mix() 与 currentColor。", default: 'var(--color-accent)(default)、var(--color-text-secondary)(subtle)、var(--color-on-dark)(onMedia)、currentColor(inherit)'},
|
|
108
109
|
{name: '--spinner-track-color', description: '圆弧所在轨道的颜色。设为 `transparent` 可得到无轨道的圆弧。onMedia 与 inherit 两种 shade 会以降低的透明度(30%)绘制轨道,以便在任意背景上可辨;该淡化同样作用于主题化的颜色。', default: 'var(--color-track)(default、subtle)、var(--color-on-dark)(onMedia)、currentColor(inherit)'},
|
|
110
|
+
{name: '--spinner-arc-fraction', description: "运动圆弧覆盖圆环的比例,为纯数字(非百分比或角度)。在尺寸变体目标上设置,方式与 --spinner-diameter 相同,例如 spinner: { 'size:xl': { '--spinner-arc-fraction': '0.75' } } 可得到 270 度的圆弧。仅在样式表加载后生效;无 CSS 的渲染(SSR、无 JS)始终绘制默认的 135 度圆弧。", default: '0.375(135 度),各尺寸相同'},
|
|
109
111
|
],
|
|
110
112
|
},
|
|
111
113
|
usage: {
|
|
@@ -136,7 +138,8 @@ export const docsDense = {
|
|
|
136
138
|
size: 'Spinner size: ring diameter (10px, 14px, 18px, 28px).',
|
|
137
139
|
shade: 'Color shade for light or dark backgrounds.',
|
|
138
140
|
label: 'Visible content below spinner. String auto-sets aria-label.',
|
|
139
|
-
'aria-label':
|
|
141
|
+
'aria-label':
|
|
142
|
+
'A11y name for screen readers. Defaults to label or a translated "Loading".',
|
|
140
143
|
xstyle: 'StyleX styles for layout customization. Must be stylex.create() value, not inline style.',
|
|
141
144
|
},
|
|
142
145
|
};
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import {describe, it, expect, vi, afterEach} from 'vitest';
|
|
13
13
|
import {render, screen} from '@testing-library/react';
|
|
14
14
|
import {Spinner} from './Spinner';
|
|
15
|
+
import {InternationalizationProvider} from '../i18n';
|
|
15
16
|
import {defineTheme} from '../theme/defineTheme';
|
|
16
17
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
17
18
|
|
|
@@ -72,6 +73,34 @@ describe('Spinner', () => {
|
|
|
72
73
|
expect(spinner).toHaveAttribute('data-shade', 'inherit');
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
it('localizes the default assistive label through the i18n catalog', () => {
|
|
77
|
+
render(
|
|
78
|
+
<InternationalizationProvider
|
|
79
|
+
locale="fr"
|
|
80
|
+
overrides={{fr: {'@astryx.spinner.loading': 'Chargement'}}}>
|
|
81
|
+
<Spinner data-testid="spinner" />
|
|
82
|
+
</InternationalizationProvider>,
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByTestId('spinner')).toHaveAttribute(
|
|
85
|
+
'aria-label',
|
|
86
|
+
'Chargement',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('keeps an explicit aria-label over the localized default', () => {
|
|
91
|
+
render(
|
|
92
|
+
<InternationalizationProvider
|
|
93
|
+
locale="fr"
|
|
94
|
+
overrides={{fr: {'@astryx.spinner.loading': 'Chargement'}}}>
|
|
95
|
+
<Spinner aria-label="Veuillez patienter" data-testid="spinner" />
|
|
96
|
+
</InternationalizationProvider>,
|
|
97
|
+
);
|
|
98
|
+
expect(screen.getByTestId('spinner')).toHaveAttribute(
|
|
99
|
+
'aria-label',
|
|
100
|
+
'Veuillez patienter',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
75
104
|
it('does not duplicate a visible string label as aria-label', () => {
|
|
76
105
|
render(<Spinner label="Fetching data" data-testid="spinner" />);
|
|
77
106
|
const status = screen.getByRole('status');
|
|
@@ -204,6 +233,14 @@ describe('Spinner', () => {
|
|
|
204
233
|
'.astryx-spinner {\n --spinner-color: var(--color-brand);',
|
|
205
234
|
);
|
|
206
235
|
});
|
|
236
|
+
|
|
237
|
+
it('scopes a themed arc fraction to that size variant (#5819)', () => {
|
|
238
|
+
expect(
|
|
239
|
+
cssFor({spinner: {'size:xl': {'--spinner-arc-fraction': '0.75'}}}),
|
|
240
|
+
).toContain(
|
|
241
|
+
'.astryx-spinner[data-size="xl"] {\n --spinner-arc-fraction: 0.75;',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
207
244
|
});
|
|
208
245
|
});
|
|
209
246
|
|
package/src/Spinner/Spinner.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Spinner.tsx
|
|
7
|
-
* @input Uses React, StyleX, SVG rendering
|
|
7
|
+
* @input Uses React, i18n (useTranslator), StyleX, SVG rendering
|
|
8
8
|
* @output Exports Spinner component, SpinnerProps, SpinnerSize, SpinnerShade types
|
|
9
9
|
* @position Core implementation of spinner loading indicator
|
|
10
10
|
*
|
|
@@ -22,6 +22,7 @@ import {colorVars, durationVars, spacingVars} from '../theme/tokens.stylex';
|
|
|
22
22
|
import type {BaseProps} from '../BaseProps';
|
|
23
23
|
import {Text} from '../Text/Text';
|
|
24
24
|
import {mergeProps} from '../utils';
|
|
25
|
+
import {useTranslator} from '../i18n';
|
|
25
26
|
import {themeProps} from '../utils/themeProps';
|
|
26
27
|
|
|
27
28
|
// =============================================================================
|
|
@@ -29,20 +30,17 @@ import {themeProps} from '../utils/themeProps';
|
|
|
29
30
|
// =============================================================================
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
* swept 135deg, not the 270deg its constant's comment claimed.
|
|
33
|
+
* Default fraction of the ring the moving arc covers. The canvas ring this
|
|
34
|
+
* replaces swept 135deg, not the 270deg its constant's comment claimed.
|
|
35
|
+
*
|
|
36
|
+
* Themeable via `--spinner-arc-fraction`, declared alongside the other public
|
|
37
|
+
* vars in `sizeStyles` below. Only the inline `strokeDasharray` attribute
|
|
38
|
+
* (the pre-stylesheet render — see its own comment) still reads this
|
|
39
|
+
* constant directly; the CSS side composes the dash from the live var.
|
|
34
40
|
*/
|
|
35
41
|
const ARC_FRACTION = 0.375;
|
|
36
42
|
|
|
37
|
-
/**
|
|
38
|
-
* The dash pattern, per unit of diameter: one arc, then the gap that closes
|
|
39
|
-
* the circle. The circumference is `pi x diameter`, so multiplying the
|
|
40
|
-
* resolved diameter by these two constants gives exactly the lengths the
|
|
41
|
-
* default render has always used, and scales them with a themed diameter.
|
|
42
|
-
*/
|
|
43
43
|
const PI = 3.141592653589793;
|
|
44
|
-
const ARC_DASH = PI * ARC_FRACTION;
|
|
45
|
-
const ARC_GAP = PI * (1 - ARC_FRACTION);
|
|
46
44
|
|
|
47
45
|
const SIZES = {
|
|
48
46
|
sm: {diameter: 10, border: 2},
|
|
@@ -322,10 +320,16 @@ const styles = stylex.create({
|
|
|
322
320
|
// of the circle — 87.398 against the 87.965 of pi x 28 — which shortens the
|
|
323
321
|
// default arc by 0.64% and moves the cap by half a pixel. Composing the
|
|
324
322
|
// lengths keeps the default byte-identical to what it drew before.
|
|
323
|
+
//
|
|
324
|
+
// The fraction itself also rides a public var (`--spinner-arc-fraction`),
|
|
325
|
+
// unregistered like the other three color/geometry public vars — it is
|
|
326
|
+
// read as a bare `<number>` multiplier here, never summed with a unitless
|
|
327
|
+
// `0` the way the registered length pair guards against, so it needs no
|
|
328
|
+
// registration.
|
|
325
329
|
arc: {
|
|
326
330
|
stroke: 'var(--spinner-color)',
|
|
327
331
|
transform: 'rotate(-90deg)',
|
|
328
|
-
strokeDasharray: `calc(var(${RESOLVED_DIAMETER}) * ${
|
|
332
|
+
strokeDasharray: `calc(var(${RESOLVED_DIAMETER}) * ${PI} * var(--spinner-arc-fraction)) calc(var(${RESOLVED_DIAMETER}) * ${PI} * (1 - var(--spinner-arc-fraction)))`,
|
|
329
333
|
},
|
|
330
334
|
track: {stroke: 'var(--spinner-track-color)'},
|
|
331
335
|
});
|
|
@@ -348,22 +352,30 @@ const styles = stylex.create({
|
|
|
348
352
|
// cost of its own — with nothing declaring the var,
|
|
349
353
|
// `theme-var-reachability.js` cannot find an element to check, so a documented
|
|
350
354
|
// var reads as unreachable.
|
|
355
|
+
// Arc fraction is not itself size-dependent, but it declares alongside the
|
|
356
|
+
// two vars that are, on the same per-size condition (`!hasLabel` gate at the
|
|
357
|
+
// call site) — it needs a home on whichever element carries the theme
|
|
358
|
+
// target, and this is the object already wired to be there.
|
|
351
359
|
const sizeStyles = stylex.create({
|
|
352
360
|
sm: {
|
|
353
361
|
'--spinner-diameter': `${SIZES.sm.diameter}px`,
|
|
354
362
|
'--spinner-stroke-width': `${SIZES.sm.border}px`,
|
|
363
|
+
'--spinner-arc-fraction': `${ARC_FRACTION}`,
|
|
355
364
|
},
|
|
356
365
|
md: {
|
|
357
366
|
'--spinner-diameter': `${SIZES.md.diameter}px`,
|
|
358
367
|
'--spinner-stroke-width': `${SIZES.md.border}px`,
|
|
368
|
+
'--spinner-arc-fraction': `${ARC_FRACTION}`,
|
|
359
369
|
},
|
|
360
370
|
lg: {
|
|
361
371
|
'--spinner-diameter': `${SIZES.lg.diameter}px`,
|
|
362
372
|
'--spinner-stroke-width': `${SIZES.lg.border}px`,
|
|
373
|
+
'--spinner-arc-fraction': `${ARC_FRACTION}`,
|
|
363
374
|
},
|
|
364
375
|
xl: {
|
|
365
376
|
'--spinner-diameter': `${SIZES.xl.diameter}px`,
|
|
366
377
|
'--spinner-stroke-width': `${SIZES.xl.border}px`,
|
|
378
|
+
'--spinner-arc-fraction': `${ARC_FRACTION}`,
|
|
367
379
|
},
|
|
368
380
|
});
|
|
369
381
|
|
|
@@ -484,6 +496,7 @@ export function Spinner({
|
|
|
484
496
|
const arcLength = circumference * ARC_FRACTION;
|
|
485
497
|
const hasLabel = label != null;
|
|
486
498
|
const labelId = useId();
|
|
499
|
+
const t = useTranslator();
|
|
487
500
|
|
|
488
501
|
// When a visible string label renders (and no explicit aria-label is set),
|
|
489
502
|
// name the status element from the visible Text via aria-labelledby instead
|
|
@@ -492,9 +505,13 @@ export function Spinner({
|
|
|
492
505
|
const namedByVisibleLabel =
|
|
493
506
|
hasLabel && typeof label === 'string' && ariaLabel == null;
|
|
494
507
|
|
|
495
|
-
// Resolve accessible name: explicit aria-label > string label >
|
|
508
|
+
// Resolve accessible name: explicit aria-label > string label > the
|
|
509
|
+
// localized default. The fallback is AT-facing text, so it goes through the
|
|
510
|
+
// translation runtime like visible text does.
|
|
496
511
|
const resolvedAriaLabel =
|
|
497
|
-
ariaLabel ??
|
|
512
|
+
ariaLabel ??
|
|
513
|
+
(typeof label === 'string' ? label : undefined) ??
|
|
514
|
+
t('@astryx.spinner.loading');
|
|
498
515
|
|
|
499
516
|
const spinner = (
|
|
500
517
|
<span
|
|
@@ -570,6 +570,139 @@ describe('ToggleButtonGroup (multiple)', () => {
|
|
|
570
570
|
});
|
|
571
571
|
});
|
|
572
572
|
|
|
573
|
+
// =============================================================================
|
|
574
|
+
// Disabled state — family:buttons FR3 (disabled means non-operable)
|
|
575
|
+
// =============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Two ways a ToggleButton becomes unavailable, and the rule that binds them:
|
|
579
|
+
* a group disables everything it contains, and a member can disable itself
|
|
580
|
+
* while the group stays enabled. Neither source may cancel the other out.
|
|
581
|
+
*
|
|
582
|
+
* The tooltip cases are here at the attribute and callback level only. A
|
|
583
|
+
* tooltip'd disabled toggle carries `aria-disabled` instead of the native
|
|
584
|
+
* `disabled` attribute, so whether a REAL mouse press is refused is an engine
|
|
585
|
+
* fact that jsdom's synthetic click cannot settle — that half lives in
|
|
586
|
+
* ./__tests__/ToggleButton.a11y.chromium.spec.ts.
|
|
587
|
+
*/
|
|
588
|
+
describe('disabled state', () => {
|
|
589
|
+
it('keeps a member disabled when the group disables nothing', async () => {
|
|
590
|
+
const user = userEvent.setup();
|
|
591
|
+
const handleChange = vi.fn();
|
|
592
|
+
render(
|
|
593
|
+
<ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
|
|
594
|
+
<ToggleButton value="list" label="List" isDisabled />
|
|
595
|
+
<ToggleButton value="grid" label="Grid" />
|
|
596
|
+
</ToggleButtonGroup>,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
expect(screen.getByRole('button', {name: 'List'})).toBeDisabled();
|
|
600
|
+
|
|
601
|
+
await user.click(screen.getByRole('button', {name: 'List'}));
|
|
602
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('leaves the rest of an enabled group selectable', async () => {
|
|
606
|
+
const user = userEvent.setup();
|
|
607
|
+
const handleChange = vi.fn();
|
|
608
|
+
render(
|
|
609
|
+
<ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
|
|
610
|
+
<ToggleButton value="list" label="List" isDisabled />
|
|
611
|
+
<ToggleButton value="grid" label="Grid" />
|
|
612
|
+
</ToggleButtonGroup>,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
expect(screen.getByRole('button', {name: 'Grid'})).toBeEnabled();
|
|
616
|
+
|
|
617
|
+
await user.click(screen.getByRole('button', {name: 'Grid'}));
|
|
618
|
+
expect(handleChange).toHaveBeenCalledWith('grid');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('still disables a member that says nothing when the group is disabled', async () => {
|
|
622
|
+
const user = userEvent.setup();
|
|
623
|
+
const handleChange = vi.fn();
|
|
624
|
+
render(
|
|
625
|
+
<ToggleButtonGroup
|
|
626
|
+
value={null}
|
|
627
|
+
onChange={handleChange}
|
|
628
|
+
label="View mode"
|
|
629
|
+
isDisabled>
|
|
630
|
+
<ToggleButton value="list" label="List" />
|
|
631
|
+
</ToggleButtonGroup>,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(screen.getByRole('button', {name: 'List'})).toBeDisabled();
|
|
635
|
+
|
|
636
|
+
await user.click(screen.getByRole('button', {name: 'List'}));
|
|
637
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('keeps a tooltip-bearing disabled member focusable but inert', async () => {
|
|
641
|
+
const user = userEvent.setup();
|
|
642
|
+
const handleChange = vi.fn();
|
|
643
|
+
render(
|
|
644
|
+
<ToggleButtonGroup value={null} onChange={handleChange} label="View mode">
|
|
645
|
+
<ToggleButton
|
|
646
|
+
value="list"
|
|
647
|
+
label="List"
|
|
648
|
+
tooltip="List view is unavailable for this dataset"
|
|
649
|
+
isDisabled
|
|
650
|
+
/>
|
|
651
|
+
</ToggleButtonGroup>,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const member = screen.getByRole('button', {name: 'List'});
|
|
655
|
+
// The tooltip is the disabled reason, so the control stays reachable to
|
|
656
|
+
// read it: aria-disabled rather than the native attribute, which would
|
|
657
|
+
// take it out of the tab order along with its own explanation.
|
|
658
|
+
expect(member).toHaveAttribute('aria-disabled', 'true');
|
|
659
|
+
expect(member).not.toBeDisabled();
|
|
660
|
+
|
|
661
|
+
await user.click(member);
|
|
662
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('does not toggle a standalone disabled toggle that carries a tooltip', async () => {
|
|
666
|
+
const user = userEvent.setup();
|
|
667
|
+
const handleChange = vi.fn();
|
|
668
|
+
render(
|
|
669
|
+
<ToggleButton
|
|
670
|
+
label="Bold"
|
|
671
|
+
tooltip="Formatting is locked for this document"
|
|
672
|
+
isPressed={false}
|
|
673
|
+
onPressedChange={handleChange}
|
|
674
|
+
isDisabled
|
|
675
|
+
/>,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
const toggle = screen.getByRole('button', {name: 'Bold'});
|
|
679
|
+
expect(toggle).toHaveAttribute('aria-disabled', 'true');
|
|
680
|
+
|
|
681
|
+
await user.click(toggle);
|
|
682
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
683
|
+
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('still toggles an enabled toggle that carries the same tooltip', async () => {
|
|
687
|
+
const user = userEvent.setup();
|
|
688
|
+
const handleChange = vi.fn();
|
|
689
|
+
render(
|
|
690
|
+
<ToggleButton
|
|
691
|
+
label="Bold"
|
|
692
|
+
tooltip="Bold the selected text"
|
|
693
|
+
isPressed={false}
|
|
694
|
+
onPressedChange={handleChange}
|
|
695
|
+
/>,
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const toggle = screen.getByRole('button', {name: 'Bold'});
|
|
699
|
+
expect(toggle).not.toHaveAttribute('aria-disabled');
|
|
700
|
+
|
|
701
|
+
await user.click(toggle);
|
|
702
|
+
expect(handleChange).toHaveBeenCalledWith(true, expect.anything());
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
573
706
|
// jsdom cannot emulate forced-colors rendering, so these assert that the
|
|
574
707
|
// compiled output includes the forced-colors rules; visual behavior needs
|
|
575
708
|
// manual verification under Windows High Contrast.
|
|
@@ -154,7 +154,8 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
156
|
* Whether the button is disabled.
|
|
157
|
-
* When used inside ToggleButtonGroup,
|
|
157
|
+
* When used inside ToggleButtonGroup, a disabled group disables this button
|
|
158
|
+
* too, but an enabled group does not re-enable a button that disables itself.
|
|
158
159
|
* @default false
|
|
159
160
|
*/
|
|
160
161
|
isDisabled?: boolean;
|
|
@@ -263,7 +264,13 @@ export function ToggleButton({
|
|
|
263
264
|
? group.selectedValues.has(value)
|
|
264
265
|
: (isPressedProp ?? false);
|
|
265
266
|
const size = sizeProp ?? group?.size ?? 'md';
|
|
266
|
-
|
|
267
|
+
// Either source disabling this button is enough. `??` could not express that:
|
|
268
|
+
// the group always supplies a boolean (its own prop defaults to false), so the
|
|
269
|
+
// fallback never ran and an enabled group handed a member that had disabled
|
|
270
|
+
// itself its availability back. A group still disables everything it contains
|
|
271
|
+
// — that is the half `??` got right — but it cannot re-enable a member
|
|
272
|
+
// (family:buttons FR3).
|
|
273
|
+
const isDisabled = (group?.isDisabled ?? false) || isDisabledProp;
|
|
267
274
|
|
|
268
275
|
// Track the pressed state optimistically so the button reflects the intended
|
|
269
276
|
// state immediately while an async action is pending. The optimistic update
|