@astryxdesign/core 0.6.1 → 0.6.2-canary.0faf070
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/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- 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/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- 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/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +19 -10
- 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/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- 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 +13 -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/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- 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/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -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/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/Markdown/Markdown.doc.mjs +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +255 -71
- 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/Outline/Outline.spec.md +1 -1
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +27 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +146 -0
- package/src/Slider/Slider.tsx +37 -13
- 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/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- 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/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -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
- package/src/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 4
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:FileInput
|
|
6
|
+
authority: current
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-09-14
|
|
11
|
+
owners: [cixzhang, imdreamrunner]
|
|
12
|
+
review_triggers: [theming]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/FileInput/FileInput.test.tsx,
|
|
16
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
17
|
+
scripts/check-knowledge.mjs,
|
|
18
|
+
]
|
|
19
|
+
modules: []
|
|
20
|
+
families: [family:input-fields]
|
|
21
|
+
design_specs: []
|
|
22
|
+
architecture: [architecture:component-theming-surface]
|
|
23
|
+
contributing: []
|
|
24
|
+
system_specs: []
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# FileInput component contract
|
|
28
|
+
|
|
29
|
+
## Intent
|
|
30
|
+
|
|
31
|
+
FileInput presents a labelled file-selection field in compact input or dropzone
|
|
32
|
+
form. This contract records its current consumer anatomy and approves separate theme
|
|
33
|
+
ownership for the upload affordance that FileInput paints through Icon.
|
|
34
|
+
|
|
35
|
+
## Compatibility and migration
|
|
36
|
+
|
|
37
|
+
- Released default preserved: `yes`
|
|
38
|
+
- Compatibility class: additive public theming target; no existing target,
|
|
39
|
+
runtime default, DOM, prop, interaction, or accessibility behavior changes
|
|
40
|
+
- Controlled/uncontrolled behavior: unchanged; FileInput remains controlled
|
|
41
|
+
- Migration decision: `component:FileInput/DEC-1`
|
|
42
|
+
|
|
43
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
44
|
+
|
|
45
|
+
## Ownership boundary
|
|
46
|
+
|
|
47
|
+
**Owns**
|
|
48
|
+
|
|
49
|
+
- The visible file-selection surface and its input/dropzone mode.
|
|
50
|
+
- Whether, where, and at what default size the upload affordance appears.
|
|
51
|
+
- Reflecting FileInput's mode on its locally owned theme targets.
|
|
52
|
+
|
|
53
|
+
**Does not own / non-goals**
|
|
54
|
+
|
|
55
|
+
- The upload artwork or Icon's base color, size, and accessibility semantics —
|
|
56
|
+
owned by `component:Icon`.
|
|
57
|
+
- Label, description, clear-control, and validation-message presentation — owned
|
|
58
|
+
by `component:Field` and `component:FieldStatus`.
|
|
59
|
+
- Loading-indicator presentation — owned by `component:Spinner`.
|
|
60
|
+
- A new prop, variant, icon slot, or custom property.
|
|
61
|
+
|
|
62
|
+
## Public concepts
|
|
63
|
+
|
|
64
|
+
No consumer prop changes. The `file-input-icon` target gives themes a
|
|
65
|
+
same-element seam for the Upload icon and reflects the existing `mode` axis. The
|
|
66
|
+
existing `file-input` target remains on the visible selection surface and keeps
|
|
67
|
+
its `mode` and `status` axes.
|
|
68
|
+
|
|
69
|
+
## Behavioral and layout contract
|
|
70
|
+
|
|
71
|
+
| ID | Candidate invariant | Basis | Review state |
|
|
72
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------- |
|
|
73
|
+
| FR1 | The visible selection surface carries `file-input` and reflects the existing `mode` and resolved status. | Current source, public docs, and focused tests | Verified current behavior |
|
|
74
|
+
| FR2 | When not loading, input mode renders an upload affordance at the small Icon size. Dropzone mode renders it at the medium Icon size only while no file is selected. | Current source and focused tests | Verified current behavior |
|
|
75
|
+
| FR3 | Icon owns the rendered glyph's base size, color, and accessibility semantics; FileInput owns the affordance's mode-dependent placement and default size. | Current composition and component boundaries | Verified current composition |
|
|
76
|
+
| FR4 | The rendered upload affordance MUST carry `file-input-icon` with the existing `mode` reflected, so a theme can restyle the glyph box without structural selectors or changing every Icon that uses the same artwork. | Owner-approved target contract | Approved additive contract |
|
|
77
|
+
| FR5 | Adding the target MUST NOT change the default artwork, computed layout, interaction, file-selection behavior, accessible name, or decorative Icon semantics. | Compatibility policy and focused regression tests | Required compatibility behavior |
|
|
78
|
+
|
|
79
|
+
### Allowed variation
|
|
80
|
+
|
|
81
|
+
- **AV1 — Theme paint.** A theme may change standard visual
|
|
82
|
+
properties such as the upload glyph's size or color through
|
|
83
|
+
`file-input-icon`; FileInput still owns whether and where the affordance renders.
|
|
84
|
+
- **AV2 — Artwork.** Icon registry and future icon-slot decisions may change the
|
|
85
|
+
artwork without changing this CSS target's ownership of the painted glyph box.
|
|
86
|
+
|
|
87
|
+
### Representative states
|
|
88
|
+
|
|
89
|
+
| State | Required invariant | Allowed variation |
|
|
90
|
+
| ------------------------ | ------------------------------------------------------------------------ | --------------------------------------- |
|
|
91
|
+
| Input, empty or selected | Small upload affordance renders on the input surface when not loading. | Files, placeholder, status, theme paint |
|
|
92
|
+
| Dropzone, empty | Medium upload affordance renders above the placeholder when not loading. | Drag state, placeholder, theme paint |
|
|
93
|
+
| Dropzone, selected | File names replace the upload affordance. | File names and status |
|
|
94
|
+
| Loading | Spinner replaces the upload affordance. | Mode and loading presentation |
|
|
95
|
+
|
|
96
|
+
### Transformation and precedence order
|
|
97
|
+
|
|
98
|
+
- **ORD1 — Content selection.** Resolve loading and selected-file state, choose
|
|
99
|
+
input or dropzone content, then render the mode-sized upload affordance only in
|
|
100
|
+
the states recorded by FR2.
|
|
101
|
+
- **ORD2 — Theme composition.** Icon applies its base size and color, then the
|
|
102
|
+
same-element FileInput target participates in the existing theme layer and
|
|
103
|
+
standard Icon styling merge order.
|
|
104
|
+
|
|
105
|
+
### Performance and resources
|
|
106
|
+
|
|
107
|
+
- **PR1 — No new work.** The additive target performs no measurement, listener,
|
|
108
|
+
observer, state update, or additional render pass.
|
|
109
|
+
|
|
110
|
+
## Accessibility contract
|
|
111
|
+
|
|
112
|
+
The upload affordance remains decorative. The existing focusable file-selection
|
|
113
|
+
trigger, label, description, required/invalid state, disabled explanation, and
|
|
114
|
+
selection announcements remain unchanged.
|
|
115
|
+
|
|
116
|
+
## Design relationships
|
|
117
|
+
|
|
118
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
119
|
+
| ---------------- | ----------------------------------------------------------------------------------- | ---------------------------------------- | -------------- | ------------------ |
|
|
120
|
+
| Drop zone | Presents the visible file-selection surface in input or dropzone form. | Current source and public docs | Prominent | FR1 |
|
|
121
|
+
| Upload icon | Hints at the upload action and changes default size with the selected mode. | Current source and owner-approved target | Supporting | FR2, FR3, FR4 |
|
|
122
|
+
| Shared feedback | Uses Field, FieldStatus, and Spinner for labels, validation, and loading treatment. | Current shared composition | Supporting | FR5 |
|
|
123
|
+
|
|
124
|
+
### Theming anatomy
|
|
125
|
+
|
|
126
|
+
<!-- anatomy-theming:v1 -->
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"Label": {
|
|
131
|
+
"delegatesTo": {"owner": "component:Field", "target": "field-label"}
|
|
132
|
+
},
|
|
133
|
+
"Description": {
|
|
134
|
+
"none": {
|
|
135
|
+
"reason": "unsettled: No current public target reaches the stable Description; future exposure still needs an owner decision"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"Drop zone": {"target": "file-input"},
|
|
139
|
+
"Upload icon": {"target": "file-input-icon"},
|
|
140
|
+
"Placeholder": {"inherits": "file-input"},
|
|
141
|
+
"File name display": {"inherits": "file-input"},
|
|
142
|
+
"Clear button": {
|
|
143
|
+
"delegatesTo": {
|
|
144
|
+
"owner": "component:Field",
|
|
145
|
+
"target": "input-clear-button"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"Spinner": {
|
|
149
|
+
"delegatesTo": {"owner": "component:Spinner", "target": "spinner"}
|
|
150
|
+
},
|
|
151
|
+
"Status message": {
|
|
152
|
+
"delegatesTo": {
|
|
153
|
+
"owner": "component:FieldStatus",
|
|
154
|
+
"target": "field-status"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The `file-input-icon` disposition records the approved target state. Icon still
|
|
161
|
+
owns the general `icon` target and base glyph semantics; FileInput's narrower
|
|
162
|
+
target owns only this stable upload position and its existing mode distinction.
|
|
163
|
+
|
|
164
|
+
## Family and system relationships
|
|
165
|
+
|
|
166
|
+
- `family:input-fields` owns shared labelled-field and validation behavior.
|
|
167
|
+
- `architecture:component-theming-surface` owns target qualification, anatomy
|
|
168
|
+
mapping, and the requirement that public targets sit on stable painted parts.
|
|
169
|
+
- Field, FieldStatus, Icon, and Spinner retain their existing public target
|
|
170
|
+
contracts when composed by FileInput.
|
|
171
|
+
|
|
172
|
+
## Verification map
|
|
173
|
+
|
|
174
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
175
|
+
| ------------------- | ----------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
176
|
+
| FR1, FR2 | `FileInput.test.tsx` rendering and target suites | Input/dropzone; empty/selected/loading | Moving the root target or changing when/at what size the affordance renders breaks focused assertions. | `audit:FileInput/theming` |
|
|
177
|
+
| FR3, FR4, FR5 | `FileInput.test.tsx`, `themingTargets.test.ts`, probe-theme check | Both modes and same-element Icon target | Missing the target, reflecting the wrong mode, or moving it off the glyph fails source/docs/probe coverage. | `audit:FileInput/theming` |
|
|
178
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Nine anatomy entries and two locally owned targets | Missing, extra, prefixed, stale, or unclaimed current mappings fail validation. | `audit:FileInput/anatomy` |
|
|
179
|
+
|
|
180
|
+
## Decision log
|
|
181
|
+
|
|
182
|
+
### DEC-1 — Upload icon is stable FileInput theme anatomy
|
|
183
|
+
|
|
184
|
+
**Reference:** `component:FileInput/DEC-1`
|
|
185
|
+
**Decider:** cixzhang, 2026-09-14
|
|
186
|
+
|
|
187
|
+
The upload icon is a stable, consumer-recognizable FileInput affordance whose
|
|
188
|
+
mode-dependent placement and default size belong to FileInput. It receives the
|
|
189
|
+
`file-input-icon` target on the same Icon element that paints the glyph, while
|
|
190
|
+
Icon retains its general target and base glyph semantics.
|
|
191
|
+
|
|
192
|
+
## Open questions
|
|
193
|
+
|
|
194
|
+
None.
|
|
195
|
+
|
|
196
|
+
## Content boundary
|
|
197
|
+
|
|
198
|
+
This file does not duplicate consumer prop tables, examples, implementation
|
|
199
|
+
steps, or shared-component contracts. It links to their owners.
|
|
@@ -119,6 +119,20 @@ describe('FileInput', () => {
|
|
|
119
119
|
expect(screen.getByText('Drop here')).toBeInTheDocument();
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it.each([
|
|
123
|
+
{mode: 'input' as const, size: 'sm'},
|
|
124
|
+
{mode: 'dropzone' as const, size: 'md'},
|
|
125
|
+
])('exposes the upload icon as a $mode theme target', ({mode, size}) => {
|
|
126
|
+
render(
|
|
127
|
+
<FileInput label="Upload" mode={mode} value={null} onChange={() => {}} />,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const icon = document.querySelector('.astryx-file-input-icon');
|
|
131
|
+
expect(icon).toHaveClass('astryx-icon');
|
|
132
|
+
expect(icon).toHaveAttribute('data-mode', mode);
|
|
133
|
+
expect(icon).toHaveAttribute('data-size', size);
|
|
134
|
+
});
|
|
135
|
+
|
|
122
136
|
it('displays selected file name', () => {
|
|
123
137
|
const file = createFile('report.pdf', 1024, 'application/pdf');
|
|
124
138
|
render(<FileInput label="Document" value={file} onChange={() => {}} />);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file FileInput.tsx
|
|
7
7
|
* @input Uses React, useId, Field, Icon, Spinner, VisuallyHidden
|
|
8
|
-
* @output Exports FileInput component,
|
|
8
|
+
* @output Exports FileInput component, public types, and its root/icon theme targets
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by FileInput.test.tsx
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
@@ -707,7 +707,12 @@ export function FileInput({
|
|
|
707
707
|
}
|
|
708
708
|
return (
|
|
709
709
|
<>
|
|
710
|
-
<Icon
|
|
710
|
+
<Icon
|
|
711
|
+
icon="arrowUp"
|
|
712
|
+
size="md"
|
|
713
|
+
color="secondary"
|
|
714
|
+
{...themeProps('file-input-icon', {mode})}
|
|
715
|
+
/>
|
|
711
716
|
<span {...stylex.props(styles.placeholderText)}>
|
|
712
717
|
{isDragOver ? t('@astryx.fileInput.dropHint') : displayPlaceholder}
|
|
713
718
|
</span>
|
|
@@ -728,7 +733,12 @@ export function FileInput({
|
|
|
728
733
|
}
|
|
729
734
|
return (
|
|
730
735
|
<>
|
|
731
|
-
<Icon
|
|
736
|
+
<Icon
|
|
737
|
+
icon="arrowUp"
|
|
738
|
+
size="sm"
|
|
739
|
+
color="secondary"
|
|
740
|
+
{...themeProps('file-input-icon', {mode})}
|
|
741
|
+
/>
|
|
732
742
|
<span
|
|
733
743
|
{...stylex.props(
|
|
734
744
|
hasFiles ? styles.fileNameText : styles.placeholderText,
|
package/src/Kbd/Kbd.doc.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export const docs = {
|
|
|
27
27
|
name: 'keys',
|
|
28
28
|
type: 'string',
|
|
29
29
|
description:
|
|
30
|
-
'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
|
|
30
|
+
'Keyboard shortcut string. Use "+" to separate keys. Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape and return for enter.',
|
|
31
31
|
required: true,
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -72,7 +72,7 @@ export const docsZh = {
|
|
|
72
72
|
name: 'keys',
|
|
73
73
|
type: 'string',
|
|
74
74
|
description:
|
|
75
|
-
'键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right。',
|
|
75
|
+
'键盘快捷键字符串。使用 "+" 分隔各按键。特殊按键:mod(Mac 上为 Cmd)、ctrl、alt、shift、enter、backspace、escape、tab、up、down、left、right、plus。别名:esc 等同于 escape,return 等同于 enter。',
|
|
76
76
|
required: true,
|
|
77
77
|
},
|
|
78
78
|
{
|
|
@@ -122,7 +122,7 @@ export const docsDense = {
|
|
|
122
122
|
],
|
|
123
123
|
},
|
|
124
124
|
propDescriptions: {
|
|
125
|
-
keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right.',
|
|
125
|
+
keys: 'Shortcut string. "+" separates keys. Special: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape, tab, up, down, left, right, plus. Aliases: esc for escape, return for enter.',
|
|
126
126
|
xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
|
|
127
127
|
className: 'CSS class for root element. Prefer xstyle; className for non-StyleX integration.',
|
|
128
128
|
style: 'Inline styles for root element. Prefer xstyle; inline styles bypass StyleX optimization.',
|
package/src/Kbd/Kbd.test.tsx
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {render, screen} from '@testing-library/react';
|
|
13
|
-
import {describe, it, expect, afterEach} from 'vitest';
|
|
13
|
+
import {describe, it, expect, afterEach, vi} from 'vitest';
|
|
14
14
|
import {Kbd} from './Kbd';
|
|
15
15
|
|
|
16
16
|
describe('Kbd', () => {
|
|
17
17
|
const originalPlatform = navigator.platform;
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
20
21
|
// Restore platform after any test that spoofs it \u2014 ensures no
|
|
21
22
|
// test pollution even if an assertion fails mid-test.
|
|
22
23
|
Object.defineProperty(navigator, 'platform', {
|
|
@@ -72,6 +73,61 @@ describe('Kbd', () => {
|
|
|
72
73
|
expect(screen.getByText('Esc')).toBeInTheDocument();
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
it.each([
|
|
77
|
+
{alias: 'esc', display: 'Esc', label: 'Escape'},
|
|
78
|
+
{alias: 'return', display: '\u21B5', label: 'Enter'},
|
|
79
|
+
])('normalizes the $alias key alias', ({alias, display, label}) => {
|
|
80
|
+
render(<Kbd keys={alias} />);
|
|
81
|
+
expect(screen.getByText(display)).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each([
|
|
86
|
+
{alias: 'esc', canonical: 'escape'},
|
|
87
|
+
{alias: 'return', canonical: 'enter'},
|
|
88
|
+
])('$alias renders identically to $canonical', ({alias, canonical}) => {
|
|
89
|
+
const {container, rerender} = render(<Kbd keys={canonical} />);
|
|
90
|
+
const text = container.textContent;
|
|
91
|
+
const label = screen.getByRole('img').getAttribute('aria-label');
|
|
92
|
+
rerender(<Kbd keys={alias} />);
|
|
93
|
+
expect(container.textContent).toBe(text);
|
|
94
|
+
expect(screen.getByRole('img').getAttribute('aria-label')).toBe(label);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it.each([
|
|
98
|
+
{keys: 'ctrl + ESC', display: 'Esc', label: 'Control + Escape'},
|
|
99
|
+
{keys: 'shift + RETURN', display: '\u21B5', label: 'Shift + Enter'},
|
|
100
|
+
])('normalizes aliases inside the $keys combo', ({keys, display, label}) => {
|
|
101
|
+
render(<Kbd keys={keys} />);
|
|
102
|
+
expect(screen.getByText(display)).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByRole('img')).toHaveAttribute('aria-label', label);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps child keys unique after alias normalization', () => {
|
|
107
|
+
const consoleError = vi
|
|
108
|
+
.spyOn(console, 'error')
|
|
109
|
+
.mockImplementation(() => {});
|
|
110
|
+
const {container} = render(
|
|
111
|
+
<Kbd keys="escape+esc+ESC+enter+return+return" />,
|
|
112
|
+
);
|
|
113
|
+
expect(consoleError).not.toHaveBeenCalled();
|
|
114
|
+
expect(
|
|
115
|
+
Array.from(container.querySelectorAll('kbd'), key => key.textContent),
|
|
116
|
+
).toEqual(['Esc', 'Esc', 'Esc', '↵', '↵', '↵']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it.each(['constructor', '__proto__'])(
|
|
120
|
+
'renders the unknown %s key without consulting object prototypes',
|
|
121
|
+
key => {
|
|
122
|
+
render(<Kbd keys={key} />);
|
|
123
|
+
expect(screen.getByText(key.toUpperCase())).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByRole('img')).toHaveAttribute(
|
|
125
|
+
'aria-label',
|
|
126
|
+
key.toUpperCase(),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
75
131
|
it('exposes a spoken accessible name and hides the glyphs (obs-1)', () => {
|
|
76
132
|
render(<Kbd keys="mod+shift+k" />);
|
|
77
133
|
// The wrapper carries a screen-reader name built from spoken key labels
|
package/src/Kbd/Kbd.tsx
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Kbd.tsx
|
|
7
|
-
* @input Uses React, StyleX, theme tokens
|
|
8
|
-
* @output Exports Kbd component and KbdProps
|
|
7
|
+
* @input Uses React, StyleX, theme tokens, shortcut key names and aliases
|
|
8
|
+
* @output Exports Kbd component and KbdProps with canonical shortcut labels
|
|
9
9
|
* @position Core implementation; renders styled keyboard shortcut indicators
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update:
|
|
@@ -61,20 +61,25 @@ const styles = stylex.create({
|
|
|
61
61
|
* Note: `mod` is not in this map — it resolves dynamically via platform
|
|
62
62
|
* detection inside the component.
|
|
63
63
|
*/
|
|
64
|
-
const KEY_DISPLAY
|
|
65
|
-
ctrl
|
|
66
|
-
alt
|
|
67
|
-
shift
|
|
68
|
-
enter
|
|
69
|
-
backspace
|
|
70
|
-
escape
|
|
71
|
-
tab
|
|
72
|
-
up
|
|
73
|
-
down
|
|
74
|
-
left
|
|
75
|
-
right
|
|
76
|
-
plus
|
|
77
|
-
|
|
64
|
+
const KEY_DISPLAY = new Map<string, string>([
|
|
65
|
+
['ctrl', '\u2303'], // ⌃
|
|
66
|
+
['alt', '\u2325'], // ⌥
|
|
67
|
+
['shift', '\u21E7'], // ⇧
|
|
68
|
+
['enter', '\u21B5'], // ↵
|
|
69
|
+
['backspace', '\u232B'], // ⌫
|
|
70
|
+
['escape', 'Esc'],
|
|
71
|
+
['tab', '\u21E5'], // ⇥
|
|
72
|
+
['up', '\u2191'],
|
|
73
|
+
['down', '\u2193'],
|
|
74
|
+
['left', '\u2190'],
|
|
75
|
+
['right', '\u2192'],
|
|
76
|
+
['plus', '+'],
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const KBD_KEY_ALIASES = new Map<string, string>([
|
|
80
|
+
['esc', 'escape'],
|
|
81
|
+
['return', 'enter'],
|
|
82
|
+
]);
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Resolves a key name to its display string. Handles the platform-aware
|
|
@@ -85,7 +90,7 @@ function getKeyDisplay(key: string, isMac: boolean): string {
|
|
|
85
90
|
if (key === 'mod') {
|
|
86
91
|
return isMac ? '\u2318' : 'Ctrl';
|
|
87
92
|
}
|
|
88
|
-
return KEY_DISPLAY
|
|
93
|
+
return KEY_DISPLAY.get(key) ?? key.toUpperCase();
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
@@ -93,26 +98,26 @@ function getKeyDisplay(key: string, isMac: boolean): string {
|
|
|
93
98
|
* (⌘, ⇧, ↵, …) that assistive tech cannot announce meaningfully, so the
|
|
94
99
|
* accessible name for the shortcut is built from these words instead.
|
|
95
100
|
*/
|
|
96
|
-
const KEY_LABEL
|
|
97
|
-
ctrl
|
|
98
|
-
alt
|
|
99
|
-
shift
|
|
100
|
-
enter
|
|
101
|
-
backspace
|
|
102
|
-
escape
|
|
103
|
-
tab
|
|
104
|
-
up
|
|
105
|
-
down
|
|
106
|
-
left
|
|
107
|
-
right
|
|
108
|
-
plus
|
|
109
|
-
|
|
101
|
+
const KEY_LABEL = new Map<string, string>([
|
|
102
|
+
['ctrl', 'Control'],
|
|
103
|
+
['alt', 'Alt'],
|
|
104
|
+
['shift', 'Shift'],
|
|
105
|
+
['enter', 'Enter'],
|
|
106
|
+
['backspace', 'Backspace'],
|
|
107
|
+
['escape', 'Escape'],
|
|
108
|
+
['tab', 'Tab'],
|
|
109
|
+
['up', 'Up arrow'],
|
|
110
|
+
['down', 'Down arrow'],
|
|
111
|
+
['left', 'Left arrow'],
|
|
112
|
+
['right', 'Right arrow'],
|
|
113
|
+
['plus', 'Plus'],
|
|
114
|
+
]);
|
|
110
115
|
|
|
111
116
|
function getKeyLabel(key: string, isMac: boolean): string {
|
|
112
117
|
if (key === 'mod') {
|
|
113
118
|
return isMac ? 'Command' : 'Control';
|
|
114
119
|
}
|
|
115
|
-
return KEY_LABEL
|
|
120
|
+
return KEY_LABEL.get(key) ?? key.toUpperCase();
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
function subscribeToPlatformChanges(): () => void {
|
|
@@ -127,7 +132,9 @@ export interface KbdProps extends BaseProps<HTMLSpanElement> {
|
|
|
127
132
|
ref?: React.Ref<HTMLSpanElement>;
|
|
128
133
|
/**
|
|
129
134
|
* Keyboard shortcut string. Use "+" to separate keys.
|
|
130
|
-
* Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape
|
|
135
|
+
* Special keys: mod (Cmd on Mac), ctrl, alt, shift, enter, backspace, escape,
|
|
136
|
+
* tab, up, down, left, and right.
|
|
137
|
+
* Aliases: "esc" for "escape" and "return" for "enter".
|
|
131
138
|
* Use "plus" to render a literal "+" key (e.g. "shift+plus").
|
|
132
139
|
*
|
|
133
140
|
* @example
|
|
@@ -163,11 +170,24 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
|
|
|
163
170
|
getServerPlatformSnapshot,
|
|
164
171
|
);
|
|
165
172
|
|
|
166
|
-
const
|
|
173
|
+
const keyOccurrences = new Map<string, number>();
|
|
174
|
+
const parts = keys
|
|
175
|
+
.split('+')
|
|
176
|
+
.map(key => key.trim().toLowerCase())
|
|
177
|
+
.map(sourceKey => {
|
|
178
|
+
const occurrence = keyOccurrences.get(sourceKey) ?? 0;
|
|
179
|
+
keyOccurrences.set(sourceKey, occurrence + 1);
|
|
180
|
+
return {
|
|
181
|
+
key: KBD_KEY_ALIASES.get(sourceKey) ?? sourceKey,
|
|
182
|
+
reactKey: `${sourceKey}:${occurrence}`,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
167
185
|
|
|
168
186
|
// Screen-reader name: the joined spoken labels (e.g. "Command + K"), since
|
|
169
187
|
// the visual glyphs below are announced meaninglessly by assistive tech.
|
|
170
|
-
const accessibleName = parts
|
|
188
|
+
const accessibleName = parts
|
|
189
|
+
.map(({key}) => getKeyLabel(key, isMac))
|
|
190
|
+
.join(' + ');
|
|
171
191
|
|
|
172
192
|
return (
|
|
173
193
|
<span
|
|
@@ -181,8 +201,8 @@ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
|
|
|
181
201
|
className,
|
|
182
202
|
style,
|
|
183
203
|
)}>
|
|
184
|
-
{parts.map(key => (
|
|
185
|
-
<kbd key={
|
|
204
|
+
{parts.map(({key, reactKey}) => (
|
|
205
|
+
<kbd key={reactKey} aria-hidden="true" {...stylex.props(styles.kbd)}>
|
|
186
206
|
{getKeyDisplay(key, isMac)}
|
|
187
207
|
</kbd>
|
|
188
208
|
))}
|
package/src/Link/Link.doc.mjs
CHANGED
|
@@ -164,6 +164,17 @@ export const docs = {
|
|
|
164
164
|
},
|
|
165
165
|
],
|
|
166
166
|
usage: {
|
|
167
|
+
accessibility: [
|
|
168
|
+
{
|
|
169
|
+
name: 'Link text',
|
|
170
|
+
category: 'Color contrast',
|
|
171
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
172
|
+
requirement: '4.5:1',
|
|
173
|
+
states: ['Rest', 'Hover', 'Pointer down'],
|
|
174
|
+
description:
|
|
175
|
+
'Link text must have at least 4.5:1 contrast with the background behind it. For Pointer down, measure against the pressed overlay the link paints behind its text.',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
167
178
|
description:
|
|
168
179
|
'A styled anchor for inline and standalone text navigation. Supports external links, underline variants, tooltips, and custom link components for router integration. Use it for navigating between pages or to external URLs.',
|
|
169
180
|
bestPractices: [
|
package/src/Link/Link.test.tsx
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {fireEvent, render, screen} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import {hasPressedArm} from '../__tests__/pressState';
|
|
15
16
|
import {Link} from './Link';
|
|
16
17
|
import {LinkProvider} from './LinkProvider';
|
|
17
18
|
|
|
@@ -417,3 +418,26 @@ describe('Link', () => {
|
|
|
417
418
|
expect(link).toHaveAttribute('data-color', 'secondary');
|
|
418
419
|
});
|
|
419
420
|
});
|
|
421
|
+
|
|
422
|
+
describe('pressed state', () => {
|
|
423
|
+
it('paints the pressed overlay behind a link while it is pressed', () => {
|
|
424
|
+
render(<Link href="/docs">Docs</Link>);
|
|
425
|
+
expect(hasPressedArm(screen.getByRole('link', {name: 'Docs'}))).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('paints it on the button form too', () => {
|
|
429
|
+
render(<Link onClick={() => {}}>Open</Link>);
|
|
430
|
+
expect(hasPressedArm(screen.getByRole('button', {name: 'Open'}))).toBe(
|
|
431
|
+
true,
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('does not press a disabled link', () => {
|
|
436
|
+
render(
|
|
437
|
+
<Link href="/docs" isDisabled>
|
|
438
|
+
Docs
|
|
439
|
+
</Link>,
|
|
440
|
+
);
|
|
441
|
+
expect(hasPressedArm(screen.getByText('Docs').closest('a')!)).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
package/src/Link/Link.tsx
CHANGED
|
@@ -45,6 +45,7 @@ import {computeTargetAndRel} from './computeTargetAndRel';
|
|
|
45
45
|
import {useInteractiveRole} from '../hooks/useInteractiveRole';
|
|
46
46
|
import {themeProps} from '../utils/themeProps';
|
|
47
47
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
48
|
+
import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
48
49
|
import {useTranslator} from '../i18n';
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -368,6 +369,9 @@ export function Link({
|
|
|
368
369
|
styles.base,
|
|
369
370
|
styles.buttonReset,
|
|
370
371
|
linkColorStyles[color],
|
|
372
|
+
// The system's pressed overlay behind the text; the hover stays the
|
|
373
|
+
// colour change above, so a press is the one background it paints.
|
|
374
|
+
!isDisabled && interactionOverlayStyles.pressedBackgroundColor,
|
|
371
375
|
hasUnderline && styles.hasUnderline,
|
|
372
376
|
isStandalone && styles.standalone,
|
|
373
377
|
isDisabled && styles.disabled,
|
|
@@ -427,6 +431,7 @@ export function Link({
|
|
|
427
431
|
focusOutlineProps.focusVisible(
|
|
428
432
|
styles.base,
|
|
429
433
|
linkColorStyles[color],
|
|
434
|
+
!isDisabled && interactionOverlayStyles.pressedBackgroundColor,
|
|
430
435
|
hasUnderline && styles.hasUnderline,
|
|
431
436
|
isStandalone && styles.standalone,
|
|
432
437
|
isDisabled && styles.disabled,
|