@astryxdesign/core 0.7.3-canary.f94e962 → 0.7.3
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 +88 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +15 -2
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +25 -2
- package/dist/Chat/ChatSystemMessage.d.ts +4 -0
- package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
- package/dist/Chat/ChatSystemMessage.js +4 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +9 -5
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +2 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -3
- package/dist/TimeInput/TimeInput.d.ts +5 -5
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +24 -2
- package/src/BottomSheet/BottomSheet.spec.md +26 -53
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +45 -48
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Button/Button.test.tsx +13 -2
- package/src/Button/Button.tsx +23 -2
- package/src/Button/__tests__/Button.a11y.states.ts +4 -3
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/Carousel/Carousel.spec.md +146 -0
- package/src/Chat/ChatComposer.spec.md +26 -26
- package/src/Chat/ChatComposer.test.tsx +58 -2
- package/src/Chat/ChatComposer.tsx +25 -1
- package/src/Chat/ChatSystemMessage.doc.mjs +1 -1
- package/src/Chat/ChatSystemMessage.test.tsx +25 -3
- package/src/Chat/ChatSystemMessage.tsx +6 -4
- package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +8 -9
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +7 -6
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +62 -7
- package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +190 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -4
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +93 -0
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +327 -0
- package/src/FieldStatus/FieldStatus.spec.md +4 -4
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +3 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +1 -2
- package/src/Markdown/Markdown.helpers.perf.test.ts +25 -19
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Switch/Switch.test.tsx +56 -0
- package/src/Switch/Switch.tsx +13 -2
- package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +11 -11
- package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +4 -3
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +8 -7
- package/src/TimeInput/TimeInput.test.tsx +119 -7
- package/src/TimeInput/TimeInput.tsx +51 -23
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.3
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -792,8 +792,6 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.3-canary.f94e962",
|
|
796
|
-
"@astryxdesign/cli": "0.7.3-canary.f94e962",
|
|
797
795
|
"@babel/cli": "^7.29.7",
|
|
798
796
|
"@babel/core": "^7.29.7",
|
|
799
797
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -804,7 +802,9 @@
|
|
|
804
802
|
"@testing-library/jest-dom": "^6.6.0",
|
|
805
803
|
"@testing-library/react": "^16.3.2",
|
|
806
804
|
"@types/babel__core": "^7.20.5",
|
|
807
|
-
"tailwindcss": "^4.3.2"
|
|
805
|
+
"tailwindcss": "^4.3.2",
|
|
806
|
+
"@astryxdesign/a11y-spec": "0.0.0",
|
|
807
|
+
"@astryxdesign/cli": "0.7.3"
|
|
808
808
|
},
|
|
809
809
|
"dependencies": {
|
|
810
810
|
"intl-messageformat": "^11.2.9"
|
|
@@ -44,6 +44,26 @@ export const CURSOR_RULES = '.cursorrules';
|
|
|
44
44
|
export const HERMES_DOT_MD = '.hermes.md';
|
|
45
45
|
export const HERMES_MD = 'HERMES.md';
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The page workflow every Astryx agent text gives, written with the command
|
|
49
|
+
* name `astryx`: the project agent docs and the computer-level skill both
|
|
50
|
+
* render these lines, so the two never drift.
|
|
51
|
+
*/
|
|
52
|
+
export const WORKFLOW_TITLE =
|
|
53
|
+
'WORKFLOW — start every page from a template, never from scratch:';
|
|
54
|
+
export const WORKFLOW_STEPS = Object.freeze([
|
|
55
|
+
'1. `astryx build "<idea>"` — names the template to scaffold from and the parts it lacks.',
|
|
56
|
+
'2. `astryx template <name> <path>` — scaffold it; keep its frame, gap and padding; replace the content.',
|
|
57
|
+
'3. `astryx template <Block>` for parts it lacks; `astryx component <Name>` to read props before using one.',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The self-check's fix for a project with no StyleX or Tailwind compiler,
|
|
62
|
+
* and for an agent outside a project.
|
|
63
|
+
*/
|
|
64
|
+
export const SELF_CHECK_CSS =
|
|
65
|
+
'replace any raw <div>/<span>, imported .css/@apply, or hardcoded #hex/px with the component or a token';
|
|
66
|
+
|
|
47
67
|
export const MARKER_START = '<!-- ASTRYX:START -->';
|
|
48
68
|
export const MARKER_END = '<!-- ASTRYX:END -->';
|
|
49
69
|
// Legacy markers — read during migration so the script finds existing XDS blocks
|
|
@@ -101,7 +121,9 @@ export function holdsAstryxBlock(content) {
|
|
|
101
121
|
while (at !== -1) {
|
|
102
122
|
const from = at + marker.length;
|
|
103
123
|
const end = content.indexOf('<!--', from);
|
|
104
|
-
const inner = (
|
|
124
|
+
const inner = (
|
|
125
|
+
end === -1 ? content.slice(from) : content.slice(from, end)
|
|
126
|
+
).trim();
|
|
105
127
|
if (!/^@\S+$/.test(inner)) return true;
|
|
106
128
|
at = content.indexOf(marker, from);
|
|
107
129
|
}
|
|
@@ -163,7 +185,7 @@ export function isAstryxPromptInReach(startDir = process.cwd()) {
|
|
|
163
185
|
* @returns {string | null}
|
|
164
186
|
*/
|
|
165
187
|
function findRepositoryRoot(start) {
|
|
166
|
-
for (let dir = start; ;
|
|
188
|
+
for (let dir = start; ;) {
|
|
167
189
|
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
168
190
|
const parent = path.dirname(dir);
|
|
169
191
|
if (parent === dir) return null;
|
|
@@ -3,11 +3,11 @@ schema_version: 3
|
|
|
3
3
|
template_version: 3
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:BottomSheet
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: imdreamrunner
|
|
10
|
+
approved_at: 2026-10-21
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [theming, layout, public-api]
|
|
13
13
|
verified_by:
|
|
@@ -15,7 +15,6 @@ verified_by:
|
|
|
15
15
|
packages/core/src/BottomSheet/BottomSheet.test.tsx,
|
|
16
16
|
packages/core/src/BottomSheet/BottomSheetPanel.test.tsx,
|
|
17
17
|
packages/core/src/BottomSheet/BottomSheetKeyboard.test.tsx,
|
|
18
|
-
packages/core/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts,
|
|
19
18
|
packages/core/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts,
|
|
20
19
|
apps/storybook/stories/BottomSheet.stories.tsx,
|
|
21
20
|
scripts/check-knowledge.mjs,
|
|
@@ -38,52 +37,24 @@ system_specs: [spec:AST-025]
|
|
|
38
37
|
## Intent
|
|
39
38
|
|
|
40
39
|
BottomSheet presents caller-provided content in a panel that rises from the
|
|
41
|
-
bottom edge. This
|
|
42
|
-
scrolling, theming reachability, and the Content
|
|
40
|
+
bottom edge. This record covers consumer anatomy, the native dialog every sheet
|
|
41
|
+
renders, keyboard-reachable scrolling, theming reachability, and the Content
|
|
42
|
+
area's container padding. Where the shipped sheet does not yet meet a
|
|
43
|
+
requirement, the row carries a present-tense implementation gap note.
|
|
43
44
|
|
|
44
45
|
## Compatibility and migration
|
|
45
46
|
|
|
46
|
-
- Released default preserved: `
|
|
47
|
-
padding, the Content area insets by `--spacing-4` on every logical edge
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
it drops that inset or sets `padding={0}`
|
|
47
|
+
- Released default preserved: `yes`; with no `padding` prop and no theme
|
|
48
|
+
padding, the Content area insets by `--spacing-4` on every logical edge, and
|
|
49
|
+
`padding={0}` keeps an unpadded Content area
|
|
50
|
+
- Compatibility class: records the shipped anatomy, padded Content area, and
|
|
51
|
+
native dialog; no public prop, default, or export changes
|
|
52
52
|
- Controlled/uncontrolled behavior: unchanged
|
|
53
|
-
- Migration decision:
|
|
53
|
+
- Migration decision: none; the precedence rule under Transformation and
|
|
54
|
+
precedence order is the whole contract
|
|
54
55
|
|
|
55
56
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
56
57
|
|
|
57
|
-
### DEP-0009 — Deprecate the unpadded default Content area of `BottomSheet`
|
|
58
|
-
|
|
59
|
-
| Field | Value |
|
|
60
|
-
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
-
| id | `DEP-0009` |
|
|
62
|
-
| cleanup | `CLN-0018` |
|
|
63
|
-
| package | `@astryxdesign/core` |
|
|
64
|
-
| surface | Default inset of the `BottomSheet` Content area when no `padding` prop and no theme padding are set |
|
|
65
|
-
| old contract | The Content area has no padding |
|
|
66
|
-
| replacement | `padding={0}` keeps the unpadded Content area; `padding` and theme padding already resolve before the default |
|
|
67
|
-
| direct authority | `component:BottomSheet` Compatibility and migration. Owners: `cixzhang` |
|
|
68
|
-
| warning | Consumer docs and the release notes name `DEP-0009` and `padding={0}`. No runtime warning (`spec:AST-017` FR29) |
|
|
69
|
-
| migration | Mechanical: add `padding={0}` to a BottomSheet that sets no padding, unless its only child is a padded Section or a Layout |
|
|
70
|
-
| codemod | `astryx upgrade` adds `padding={0}` before any spread, skips sheets whose only child is a core Section or Layout, and marks a nested padding-less Layout for review |
|
|
71
|
-
| downstream | In-repo sheets that supply their own inset pass `padding={0}`; consumer examples rely on the default inset |
|
|
72
|
-
| transition | no prior transition |
|
|
73
|
-
| state | `proposed` |
|
|
74
|
-
| open decision | This record is draft and needs approval |
|
|
75
|
-
| target plan | `CLN-0018` in the next minor release |
|
|
76
|
-
|
|
77
|
-
### CLN-0018 — BottomSheet pads its Content area by default
|
|
78
|
-
|
|
79
|
-
| Field | Value |
|
|
80
|
-
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
81
|
-
| id | `CLN-0018` |
|
|
82
|
-
| lifecycle | `DEP-0009` |
|
|
83
|
-
| delta | With no `padding` prop and no theme padding, the Content area insets by `--spacing-4` on every logical edge and publishes that inset |
|
|
84
|
-
| rollback | Restore the unpadded default from the final-patch baseline |
|
|
85
|
-
| state | `pending` — lands in a declared minor with its `[breaking]` Changeset |
|
|
86
|
-
|
|
87
58
|
## Ownership boundary
|
|
88
59
|
|
|
89
60
|
**Owns**
|
|
@@ -95,7 +66,9 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
95
66
|
`architecture:container-padding`.
|
|
96
67
|
- Standalone sheet presentation, including its optional native-dialog Scrim.
|
|
97
68
|
- The native dialog every sheet renders, standalone or switcher-managed
|
|
98
|
-
(`component:BottomSheetSwitcher` DEC-1
|
|
69
|
+
(`component:BottomSheetSwitcher` DEC-1). Implementation gap: a
|
|
70
|
+
switcher-managed sheet renders a panel inside the switcher's one shared
|
|
71
|
+
dialog and owns no dialog of its own.
|
|
99
72
|
|
|
100
73
|
**Does not own / non-goals**
|
|
101
74
|
|
|
@@ -104,7 +77,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
104
77
|
`main`.
|
|
105
78
|
- The presentation lifecycle of its dialog and the flow's scrim ownership when
|
|
106
79
|
the sheet participates in a BottomSheetSwitcher — owned by BottomSheetSwitcher
|
|
107
|
-
(`component:BottomSheetSwitcher` DEC-1
|
|
80
|
+
(`component:BottomSheetSwitcher` DEC-1).
|
|
108
81
|
- Shared layer hosting or dismissal policy.
|
|
109
82
|
|
|
110
83
|
## Public concepts
|
|
@@ -114,12 +87,12 @@ in `BottomSheet.doc.mjs`.
|
|
|
114
87
|
|
|
115
88
|
## Behavioral and layout contract
|
|
116
89
|
|
|
117
|
-
| ID |
|
|
118
|
-
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
-
| FR1 | The current presented sheet contains one Sheet panel, one scrolling Content area, and one decorative Handle; a Scrim is present only in scrim-backed presentation. | Current source, docs, and tests
|
|
120
|
-
| FR2 | The Sheet panel carries `bottom-sheet`; Content area, Handle, and Scrim carry no BottomSheet public target. | Current source, docs, and tests
|
|
121
|
-
| FR3 | Content renders in one real observed content box. Effective overflow, containment, and keyboard access come from `useScrollableArea`; the named overflowing body delegates forward Tab entry to a safe first sequential native link/button. | `spec:AST-025`;
|
|
122
|
-
| FR4 | The Content area pads caller content by the sheet's container inset and publishes the applied inset to descendants as a container publisher. | `architecture:container-padding`
|
|
90
|
+
| ID | Invariant | Basis |
|
|
91
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
92
|
+
| FR1 | The current presented sheet contains one Sheet panel, one scrolling Content area, and one decorative Handle; a Scrim is present only in scrim-backed presentation. | Current source, docs, and tests |
|
|
93
|
+
| FR2 | The Sheet panel carries `bottom-sheet`; Content area, Handle, and Scrim carry no BottomSheet public target. | Current source, docs, and tests |
|
|
94
|
+
| FR3 | Content renders in one real observed content box. Effective overflow, containment, and keyboard access come from `useScrollableArea`; the named overflowing body delegates forward Tab entry to a safe first sequential native link/button. | `spec:AST-025`; tests |
|
|
95
|
+
| FR4 | The Content area pads caller content by the sheet's container inset and publishes the applied inset to descendants as a container publisher. | `architecture:container-padding` |
|
|
123
96
|
|
|
124
97
|
### Allowed variation
|
|
125
98
|
|
|
@@ -134,8 +107,8 @@ in `BottomSheet.doc.mjs`.
|
|
|
134
107
|
without a Scrim.
|
|
135
108
|
- A switcher-managed sheet renders the same panel anatomy in its own dialog while
|
|
136
109
|
BottomSheetSwitcher coordinates that dialog's lifecycle and the flow's one Scrim
|
|
137
|
-
(`component:BottomSheetSwitcher` DEC-1
|
|
138
|
-
|
|
110
|
+
(`component:BottomSheetSwitcher` DEC-1). Implementation gap: the panel
|
|
111
|
+
renders inside the switcher's one shared dialog.
|
|
139
112
|
|
|
140
113
|
### Transformation and precedence order
|
|
141
114
|
|
|
@@ -498,6 +498,35 @@ describe('BottomSheet', () => {
|
|
|
498
498
|
expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
+
it('stays open when a file picker inside it is dismissed, and still closes on its own close request', () => {
|
|
502
|
+
// A file input fires a bubbling `cancel` at itself when its picker is
|
|
503
|
+
// dismissed. It reaches the <dialog> like a close request but is not one.
|
|
504
|
+
const onOpenChange = vi.fn();
|
|
505
|
+
render(
|
|
506
|
+
<BottomSheet
|
|
507
|
+
isOpen
|
|
508
|
+
purpose="form"
|
|
509
|
+
onOpenChange={onOpenChange}
|
|
510
|
+
label="Upload">
|
|
511
|
+
<input type="file" aria-label="Picture" />
|
|
512
|
+
</BottomSheet>,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
|
|
516
|
+
fireEvent(screen.getByLabelText('Picture'), fromPicker);
|
|
517
|
+
|
|
518
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
519
|
+
expect(fromPicker.defaultPrevented).toBe(false);
|
|
520
|
+
|
|
521
|
+
fireEvent(
|
|
522
|
+
screen.getByRole('dialog'),
|
|
523
|
+
new Event('cancel', {cancelable: true}),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
|
527
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
528
|
+
});
|
|
529
|
+
|
|
501
530
|
it('keeps a standalone modal sheet presented through its exit animation', () => {
|
|
502
531
|
render(<ExitHarness />);
|
|
503
532
|
const dialog = screen.getByRole('dialog', {name: 'Filters'});
|
|
@@ -361,12 +361,16 @@ function StandaloneBottomSheet({
|
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
// The native `cancel` event is a close request the stack never saw a press
|
|
364
|
-
// for (Android back, the platform close watcher).
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
364
|
+
// for (Android back, the platform close watcher). One that bubbles up from
|
|
365
|
+
// inside is not: a file input fires it when its picker is dismissed, so it is
|
|
366
|
+
// left alone. Otherwise always preventDefault so the browser cannot close the
|
|
367
|
+
// controlled <dialog>, then answer with the stack's rules: only the top-most
|
|
368
|
+
// layer dismisses, and nothing dismisses during an IME composition.
|
|
368
369
|
const handleCancel = useCallback(
|
|
369
370
|
(event: React.SyntheticEvent<HTMLDialogElement>) => {
|
|
371
|
+
if (event.target !== event.currentTarget) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
370
374
|
event.preventDefault();
|
|
371
375
|
if (shouldDismissOnCloseRequest()) {
|
|
372
376
|
dismissOnEscape();
|
|
@@ -3,17 +3,16 @@ schema_version: 3
|
|
|
3
3
|
template_version: 4
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:BottomSheetSwitcher
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: imdreamrunner
|
|
10
|
+
approved_at: 2026-10-21
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
13
13
|
verified_by:
|
|
14
14
|
[
|
|
15
15
|
packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx,
|
|
16
|
-
packages/core/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts,
|
|
17
16
|
apps/storybook/stories/BottomSheetSwitcher.stories.tsx,
|
|
18
17
|
scripts/check-knowledge.mjs,
|
|
19
18
|
]
|
|
@@ -35,13 +34,10 @@ system_specs: [spec:AST-002, spec:AST-027]
|
|
|
35
34
|
## Intent
|
|
36
35
|
|
|
37
36
|
BottomSheetSwitcher coordinates a mutually exclusive sequence of BottomSheet
|
|
38
|
-
children
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
is "Owner decision" is the decided requirement, not shipped behavior; the
|
|
43
|
-
representative states that name a sheet's own dialog, the retained dialog, or
|
|
44
|
-
the flow wrapper describe DEC-1's decided structure in the same way.
|
|
37
|
+
children, each presented in its own native dialog (DEC-1), without adding a
|
|
38
|
+
prop or changing a default. Where the shipped switcher does not yet meet a
|
|
39
|
+
requirement, the row carries a present-tense implementation gap note. The
|
|
40
|
+
modality, identifier, and theming questions below stay open.
|
|
45
41
|
|
|
46
42
|
## Compatibility and migration
|
|
47
43
|
|
|
@@ -61,6 +57,8 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
61
57
|
|
|
62
58
|
- The presentation lifecycle of every directly nested BottomSheet child's native
|
|
63
59
|
dialog: which is presented, retained during a handoff, or closed.
|
|
60
|
+
Implementation gap: the switcher renders one dialog of its own and hosts
|
|
61
|
+
every child as a panel inside it; no child owns a dialog yet.
|
|
64
62
|
- Which child sheet is active, retained during a handoff, or hidden.
|
|
65
63
|
- Which sheet dialog hosts the flow (its ref, `id`, and dismissal
|
|
66
64
|
registration) and which paints the flow's one scrim, plus
|
|
@@ -81,30 +79,27 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
81
79
|
|
|
82
80
|
## Public concepts
|
|
83
81
|
|
|
84
|
-
| Concept | Closed values or states | Meaning
|
|
85
|
-
| ------------------- | ---------------------------------------------------- |
|
|
86
|
-
| Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow
|
|
87
|
-
| Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host
|
|
88
|
-
| Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child
|
|
89
|
-
| Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller
|
|
90
|
-
| Dialog surface | inherited dialog props plus `ref` and `onCancel` | Dialog props and handlers, `onCancel` included, reach every sheet dialog; `ref`, `id`, and `data-testid` resolve to the host dialog, which is never null for an object ref while a sheet is mounted (
|
|
82
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
83
|
+
| ------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
|
84
|
+
| Active sheet | `null` or one nested BottomSheet `sheetId` | Selects the only interactive child, or closes the flow | All presentations | `null` is closed; the prop is required | `component:BottomSheetSwitcher` | released | A non-matching or duplicate id is not given a safe public fallback by the current contract |
|
|
85
|
+
| Shared presentation | `true` / `false` through `hasScrim` | Selects the current modal scrim-backed host or non-modal no-scrim host | Entire flow | `true` | `component:BottomSheetSwitcher` | released | Boolean only |
|
|
86
|
+
| Child identity | Non-empty `sheetId` on each direct BottomSheet child | Associates controlled selection, labeling, purpose, and transition state with a child | Direct nested BottomSheet children | none | `component:BottomSheet` | released | Empty ids warn and stay hidden; duplicate-id behavior is not specified |
|
|
87
|
+
| Dismissal request | `onActiveSheetChange(null)` | Reports an allowed implicit close without taking control from the caller | Escape/platform close, scrim click, or swipe according to active child purpose | none | `component:BottomSheetSwitcher`; `family:overlay-dismissal` owns ordering | released | The caller may retain its controlled value |
|
|
88
|
+
| Dialog surface | inherited dialog props plus `ref` and `onCancel` | Dialog props and handlers, `onCancel` included, reach every sheet dialog; `ref`, `id`, and `data-testid` resolve to the host dialog, which is never null for an object ref while a sheet is mounted (DEC-2). Implementation gap: every dialog prop, `ref`, `id`, and `data-testid` reaches the switcher's one shared dialog | Entire flow | none | `component:BottomSheetSwitcher` | released | Component-owned semantics and handlers retain precedence |
|
|
91
89
|
|
|
92
90
|
## Behavioral and layout contract
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
| FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test | Settled remediation in this audit |
|
|
106
|
-
| FR8 | One backdrop paints the flow's scrim at a time: the retained sheet's dialog keeps it through a handoff and the entering dialog takes it over without a fade when the retained dialog closes. A stale gesture from an outgoing sheet cannot change the flow's scrim, and unmounting a retained sheet cannot keep its dialog open or the flow's modal layer held. | DEC-1; current tests | Owner decision |
|
|
107
|
-
| FR9 | The switcher forwards neutral dialog attributes, styling inputs, and composed handlers, `onCancel` included, to every sheet dialog, so a handler fires for whichever sheet dialog raises the event; renders `id` and `data-testid` on the host dialog only, so each appears once in the document; attaches its ref to the host dialog; and preserves its owned ARIA and dismissal behavior. An object ref's `current` is never `null` while a sheet is mounted. A callback ref follows React's semantics on a handoff: a cleanup it returned runs when it detaches from the old host; a callback ref without a cleanup is called with `null` on detach, then with the new host. | DEC-1, DEC-2; `architecture:public-component-api` INV8 | Owner decision |
|
|
92
|
+
| ID | Invariant | Basis |
|
|
93
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
94
|
+
| FR1 | Zero or one child sheet is interactive for each controlled `activeSheet` value; all other children are hidden or retained inert and `aria-hidden` during a transition. | Current source, docs, and tests |
|
|
95
|
+
| FR2 | Each child sheet owns one native dialog. Opening a sheet presents its dialog once; a handoff presents the entering sheet's dialog above the retained one and closes the retained dialog only after its panel has faded. Returning to a sheet whose dialog is still open beneath a newer one re-presents it so the interactive sheet is the topmost layer. Implementation gap: the switcher presents one shared dialog once per flow and hosts every child as a panel inside it; a handoff swaps panels in that dialog, and no dialog is re-presented. | DEC-1 |
|
|
96
|
+
| FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. | Current source and transition tests |
|
|
97
|
+
| FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests |
|
|
98
|
+
| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. Implementation gap: a script focus trap runs on top of the modal dialog's native containment; a non-modal flow is one shared dialog carrying the page-level value itself, with no flow wrapper. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |
|
|
99
|
+
| FR6 | The active child purpose governs implicit dismissal: `info` allows Escape, scrim click, and swipe; `form` allows Escape; `required` blocks all three and exposes `alertdialog`. | Current BottomSheet docs and switcher tests |
|
|
100
|
+
| FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test |
|
|
101
|
+
| FR8 | One backdrop paints the flow's scrim at a time: the retained sheet's dialog keeps it through a handoff and the entering dialog takes it over without a fade when the retained dialog closes. A stale gesture from an outgoing sheet cannot change the flow's scrim, and unmounting a retained sheet cannot keep its dialog open or the flow's modal layer held. Implementation gap: the one backdrop is the shared dialog's, so no takeover between dialogs happens. | DEC-1; current tests |
|
|
102
|
+
| FR9 | The switcher forwards neutral dialog attributes, styling inputs, and composed handlers, `onCancel` included, to every sheet dialog, so a handler fires for whichever sheet dialog raises the event; renders `id` and `data-testid` on the host dialog only, so each appears once in the document; attaches its ref to the host dialog; and preserves its owned ARIA and dismissal behavior. An object ref's `current` is never `null` while a sheet is mounted. A callback ref follows React's semantics on a handoff: a cleanup it returned runs when it detaches from the old host; a callback ref without a cleanup is called with `null` on detach, then with the new host. Implementation gap: every forwarded input, `ref`, `id`, and `data-testid` reaches the one shared dialog, which never changes on a handoff. | DEC-1, DEC-2; `architecture:public-component-api` INV8 |
|
|
108
103
|
|
|
109
104
|
### Allowed variation
|
|
110
105
|
|
|
@@ -116,8 +111,9 @@ intentional decision. This draft cannot clear the gaps it records.
|
|
|
116
111
|
|
|
117
112
|
### Representative states
|
|
118
113
|
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
Where a state names a sheet's own dialog, the retained dialog, or the flow
|
|
115
|
+
wrapper, the shipped switcher meets it with its one shared dialog instead; FR2
|
|
116
|
+
and FR5 record that implementation gap.
|
|
121
117
|
|
|
122
118
|
| State | Required invariant | Allowed variation |
|
|
123
119
|
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
@@ -125,7 +121,7 @@ own dialog, the retained dialog, or the flow wrapper.
|
|
|
125
121
|
| First open | Matching child is visible and interactive in its own dialog. | Child-owned height and content. |
|
|
126
122
|
| Handoff | Entering child is interactive in its own dialog; retained child's dialog stays open, inert, and hidden from accessibility APIs until it fades, and keeps painting the one scrim. | Alignment offset depends on measured panel geometry. |
|
|
127
123
|
| Closing | Outgoing child is retained inert while exit motion completes; modal focus and scroll ownership remain until close. | Theme-controlled duration. |
|
|
128
|
-
| Modal | Native modal host, backdrop, native focus containment, and the page scroll lock are active.
|
|
124
|
+
| Modal | Native modal host, backdrop, native focus containment, and the page scroll lock are active. Implementation gap: a script focus trap runs on top of the native containment. | Active child's `purpose` controls implicit dismissal. |
|
|
129
125
|
| Non-modal | Native non-modal host has no backdrop or scroll lock and background content remains interactive. | The current clipping/stacking limitation is a conformance gap, not an allowed exception. |
|
|
130
126
|
|
|
131
127
|
### Transformation and precedence order
|
|
@@ -144,7 +140,8 @@ own dialog, the retained dialog, or the flow wrapper.
|
|
|
144
140
|
Native modality holds across a modal flow's child handoffs because a sheet
|
|
145
141
|
dialog is modal throughout, and the one page scroll lock is held for the
|
|
146
142
|
whole flow rather than per step. A non-modal flow paints no backdrop and
|
|
147
|
-
holds no lock.
|
|
143
|
+
holds no lock. Implementation gap: one shared dialog is presented
|
|
144
|
+
throughout, so a handoff has one dialog open, not two.
|
|
148
145
|
- **PR2 — Stable child registration.** Parent rerenders and changing consumer ref
|
|
149
146
|
identities do not unregister a mounted child or cancel an active handoff.
|
|
150
147
|
|
|
@@ -160,11 +157,11 @@ own dialog, the retained dialog, or the flow wrapper.
|
|
|
160
157
|
|
|
161
158
|
## Design relationships
|
|
162
159
|
|
|
163
|
-
| Anatomy or state | Design requirement
|
|
164
|
-
| ---------------- |
|
|
165
|
-
| Shared dialog | Transparent host for the flow's interaction and accessibility boundary
|
|
166
|
-
| Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet
|
|
167
|
-
| Scrim | Optional native backdrop that dims and blocks the page in modal presentation;
|
|
160
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
161
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | -------------- | ------------------ |
|
|
162
|
+
| Shared dialog | Transparent host for the flow's interaction and accessibility boundary, one per child sheet (DEC-1). Implementation gap: one such host serves the whole flow | Current source and public docs; DEC-1 for the per-sheet clause | Structural | FR2, FR5, AR1 |
|
|
163
|
+
| Sheet panels | Delegate visible panel, content, handle, and gesture presentation to BottomSheet | `component:BottomSheet` as observational context | Prominent | FR1, FR3 |
|
|
164
|
+
| Scrim | Optional native backdrop that dims and blocks the page in modal presentation; one sheet dialog paints it at a time (DEC-1) | Current source and public docs; DEC-1 for the one-painter clause | Supporting | FR5, FR8 |
|
|
168
165
|
|
|
169
166
|
### Theming anatomy
|
|
170
167
|
|
|
@@ -198,16 +195,16 @@ own dialog, the retained dialog, or the flow wrapper.
|
|
|
198
195
|
- `architecture:public-component-api` and `spec:AST-002` own the released prop,
|
|
199
196
|
DOM, ref, and compatibility surface.
|
|
200
197
|
- `spec:AST-027` identifies the non-modal `show()` plus page-level `z-index` path
|
|
201
|
-
as a migration gap
|
|
198
|
+
as a migration gap, not an exception.
|
|
202
199
|
|
|
203
200
|
## Verification map
|
|
204
201
|
|
|
205
|
-
| Contract | Verification
|
|
206
|
-
| ---------------- |
|
|
207
|
-
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx`
|
|
208
|
-
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
|
|
209
|
-
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks
|
|
210
|
-
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests
|
|
202
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
203
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
204
|
+
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close | A child becomes interactive at the wrong time, a retained child disappears early, a second backdrop paints, or a sheet dialog's lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
|
|
205
|
+
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` (implementation gap: not yet in the tree) | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
|
|
206
|
+
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref across open/handoff/close/unmount, callback-ref cleanup, `id` once, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or duplicated, an object ref is null while a sheet is mounted or skips a cleanup, or an input overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
|
|
207
|
+
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
|
|
211
208
|
|
|
212
209
|
## Decision log
|
|
213
210
|
|
|
@@ -708,6 +708,32 @@ describe('BottomSheetSwitcher', () => {
|
|
|
708
708
|
expect(onActiveSheetChange).toHaveBeenNthCalledWith(2, null);
|
|
709
709
|
});
|
|
710
710
|
|
|
711
|
+
it('keeps the flow open when a file picker inside a step is dismissed, and still closes on its own close request', () => {
|
|
712
|
+
// A file input fires a bubbling `cancel` at itself when its picker is
|
|
713
|
+
// dismissed. A step renders no dialog, so it reaches the switcher's.
|
|
714
|
+
const onActiveSheetChange = vi.fn();
|
|
715
|
+
render(
|
|
716
|
+
<BottomSheetSwitcher
|
|
717
|
+
activeSheet="upload"
|
|
718
|
+
onActiveSheetChange={onActiveSheetChange}>
|
|
719
|
+
<BottomSheet sheetId="upload" label="Upload" purpose="form">
|
|
720
|
+
<input type="file" aria-label="Picture" />
|
|
721
|
+
</BottomSheet>
|
|
722
|
+
</BottomSheetSwitcher>,
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
|
|
726
|
+
fireEvent(screen.getByLabelText('Picture'), fromPicker);
|
|
727
|
+
|
|
728
|
+
expect(onActiveSheetChange).not.toHaveBeenCalled();
|
|
729
|
+
expect(fromPicker.defaultPrevented).toBe(false);
|
|
730
|
+
|
|
731
|
+
fireEvent(getSharedDialog(), new Event('cancel', {cancelable: true}));
|
|
732
|
+
|
|
733
|
+
expect(onActiveSheetChange).toHaveBeenCalledTimes(1);
|
|
734
|
+
expect(onActiveSheetChange).toHaveBeenCalledWith(null);
|
|
735
|
+
});
|
|
736
|
+
|
|
711
737
|
it('honors purpose=required for a switcher-managed sheet', () => {
|
|
712
738
|
const onActiveSheetChange = vi.fn();
|
|
713
739
|
render(
|
|
@@ -591,8 +591,14 @@ export function BottomSheetSwitcher({
|
|
|
591
591
|
? undefined
|
|
592
592
|
: sheetLabels.get(visibleTransition.retainedSheet));
|
|
593
593
|
|
|
594
|
+
// A `cancel` that bubbles up from a step is not a close request for the
|
|
595
|
+
// flow: a file input fires one when its picker is dismissed, and a step
|
|
596
|
+
// renders no dialog of its own, so it lands here. It is left alone.
|
|
594
597
|
const handleCancel = useCallback(
|
|
595
598
|
(event: SyntheticEvent<HTMLDialogElement>) => {
|
|
599
|
+
if (event.target !== event.currentTarget) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
596
602
|
event.preventDefault();
|
|
597
603
|
if (shouldDismissOnCloseRequest()) {
|
|
598
604
|
dismissOnEscape();
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
fireEvent,
|
|
18
18
|
createEvent,
|
|
19
19
|
act,
|
|
20
|
+
waitFor,
|
|
20
21
|
} from '@testing-library/react';
|
|
21
22
|
import userEvent from '@testing-library/user-event';
|
|
22
23
|
import {Button} from './Button';
|
|
@@ -643,7 +644,17 @@ describe('Button', () => {
|
|
|
643
644
|
expect(liveRegion).toHaveTextContent('Loading');
|
|
644
645
|
});
|
|
645
646
|
|
|
646
|
-
it('
|
|
647
|
+
it('mounts the loading live region empty when first rendered loading, then fills it (spec:AST-086 FR3)', async () => {
|
|
648
|
+
render(<Button label="Submit" isLoading />);
|
|
649
|
+
const button = screen.getByRole('button');
|
|
650
|
+
// The Spinner also has role="status", so grab the live region explicitly.
|
|
651
|
+
const regions = button.querySelectorAll('[role="status"]');
|
|
652
|
+
const liveRegion = regions[regions.length - 1];
|
|
653
|
+
expect(liveRegion).toHaveTextContent('');
|
|
654
|
+
await waitFor(() => expect(liveRegion).toHaveTextContent('Loading'));
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('localizes the loading announcement through the i18n catalog', async () => {
|
|
647
658
|
render(
|
|
648
659
|
<InternationalizationProvider
|
|
649
660
|
locale="fr"
|
|
@@ -655,7 +666,7 @@ describe('Button', () => {
|
|
|
655
666
|
// The Spinner also has role="status", so grab the live region explicitly.
|
|
656
667
|
const regions = button.querySelectorAll('[role="status"]');
|
|
657
668
|
const liveRegion = regions[regions.length - 1];
|
|
658
|
-
expect(liveRegion).toHaveTextContent('Chargement');
|
|
669
|
+
await waitFor(() => expect(liveRegion).toHaveTextContent('Chargement'));
|
|
659
670
|
});
|
|
660
671
|
|
|
661
672
|
describe('in a narrow row', () => {
|