@astryxdesign/core 0.7.3 → 0.7.4-canary.3f6ea7d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BottomSheet/BottomSheet.d.ts +18 -6
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +268 -58
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +41 -12
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +294 -165
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +51 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +7 -11
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +46 -7
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +12 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +6 -1
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +6 -1
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +9 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +6 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +6 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +9 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +6 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +6 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +9 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +9 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +7 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +9 -1
- package/dist/astryx.css +2 -0
- package/dist/hooks/useBusyAnnouncement.d.ts +22 -0
- package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -0
- package/dist/hooks/useBusyAnnouncement.js +87 -0
- package/dist/hooks/useMenuPress.d.ts +2 -2
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +5 -1
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +7 -2
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +1 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
- package/locales/en.json +6 -2
- package/locales/pseudo.json +3 -0
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +2 -2
- package/src/BottomSheet/BottomSheet.spec.md +2 -5
- package/src/BottomSheet/BottomSheet.tsx +260 -47
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -2
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +11 -10
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +28 -35
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +830 -48
- package/src/BottomSheet/BottomSheetSwitcher.tsx +396 -215
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +57 -2
- package/src/Carousel/Carousel.test.tsx +8 -12
- package/src/Carousel/Carousel.tsx +6 -13
- package/src/Chat/ChatComposerInput.test.tsx +167 -1
- package/src/Chat/useTriggerMenu.tsx +55 -6
- package/src/ComplexSelector/ComplexSelector.tsx +5 -1
- package/src/ContextMenu/ContextMenu.test.tsx +34 -0
- package/src/ContextMenu/ContextMenu.tsx +13 -1
- package/src/DateInput/DateInput.tsx +5 -1
- package/src/DateInput/DateInputTouch.test.tsx +45 -0
- package/src/DateInput/NativeDateField.test.tsx +10 -2
- package/src/DateInput/NativeDateField.tsx +5 -1
- package/src/DateInput/TouchDateField.tsx +14 -2
- package/src/DateRangeInput/DateRangeInput.tsx +5 -1
- package/src/DateTimeInput/DateTimeInput.tsx +5 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
- package/src/MultiSelector/MultiSelector.test.tsx +18 -4
- package/src/MultiSelector/MultiSelector.tsx +5 -1
- package/src/ProgressBar/ProgressBar.test.tsx +4 -21
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +114 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +12 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +38 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +123 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +107 -0
- package/src/Selector/Selector.test.tsx +22 -5
- package/src/Selector/Selector.tsx +5 -1
- package/src/TextArea/TextArea.test.tsx +9 -1
- package/src/TextArea/TextArea.tsx +5 -1
- package/src/TextInput/TextInput.tsx +5 -1
- package/src/TimeInput/TimeInput.tsx +5 -1
- package/src/TimeInput/TouchTimeField.tsx +12 -2
- package/src/Tokenizer/Tokenizer.test.tsx +17 -12
- package/src/Tokenizer/Tokenizer.tsx +10 -1
- package/src/Typeahead/BaseTypeahead.tsx +14 -1
- package/src/Typeahead/Typeahead.test.tsx +32 -41
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +470 -0
- package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +50 -0
- package/src/hooks/useBusyAnnouncement.ts +100 -0
- package/src/hooks/useMenuPress.test.tsx +27 -1
- package/src/hooks/useMenuPress.ts +12 -3
- package/src/hooks/useTypeahead.test.tsx +42 -1
- package/src/hooks/useTypeahead.ts +7 -2
- package/src/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
|
@@ -315,6 +315,7 @@ const runtimeCatalog = {
|
|
|
315
315
|
'@astryx.chatToolCalls.status.complete': '⟦Çóɱþłéţé⟧',
|
|
316
316
|
'@astryx.chatToolCalls.status.error': '⟦Ƒàíłéð⟧',
|
|
317
317
|
'@astryx.chatToolCalls.groupLabel': '⟦{count} ţóół çàłłš⟧',
|
|
318
|
+
'@astryx.chatTriggerMenu.searching': '⟦Šéàřçĥíñĝ…⟧',
|
|
318
319
|
'@astryx.chatTriggerMenu.suggestions': '⟦Šúĝĝéšţíóñš⟧',
|
|
319
320
|
'@astryx.citation.label': '⟦Çíţàţíóñ {number}: {title}⟧',
|
|
320
321
|
'@astryx.codeBlock.copied': '⟦Çóþíéð⟧',
|
package/locales/en.json
CHANGED
|
@@ -901,7 +901,7 @@
|
|
|
901
901
|
},
|
|
902
902
|
"@astryx.typeahead.loading": {
|
|
903
903
|
"defaultMessage": "Loading",
|
|
904
|
-
"description": "Screen-reader-only name for
|
|
904
|
+
"description": "Screen-reader-only name for the spinner a Typeahead, Tokenizer or directly used BaseTypeahead shows while suggestions are being fetched; also announced once through the shared announcement channel when each search starts. Present-progressive form."
|
|
905
905
|
},
|
|
906
906
|
"@astryx.typeahead.searchResults": {
|
|
907
907
|
"defaultMessage": "Search results",
|
|
@@ -1169,7 +1169,7 @@
|
|
|
1169
1169
|
},
|
|
1170
1170
|
"@astryx.spinner.loading": {
|
|
1171
1171
|
"defaultMessage": "Loading",
|
|
1172
|
-
"description": "Screen-reader-only default name for a standalone Spinner's role=\"status\" element, used when the consumer supplies neither `aria-label` nor a visible string label. Present-progressive form. Kept separate from `button.loading` and `typeahead.loading` — translations may diverge."
|
|
1172
|
+
"description": "Screen-reader-only default name for a standalone Spinner's role=\"status\" element, used when the consumer supplies neither `aria-label` nor a visible string label; also announced once when a busy input or selector starts loading, in place of its decorative Spinner. Present-progressive form. Kept separate from `button.loading` and `typeahead.loading` — translations may diverge."
|
|
1173
1173
|
},
|
|
1174
1174
|
"@astryx.chatComposerDrawer.expand": {
|
|
1175
1175
|
"defaultMessage": "Expand {label}",
|
|
@@ -1231,6 +1231,10 @@
|
|
|
1231
1231
|
"defaultMessage": "{count} tool calls",
|
|
1232
1232
|
"description": "Summary label shown in the group header when multiple tool calls are expanded. {count} is the number of calls."
|
|
1233
1233
|
},
|
|
1234
|
+
"@astryx.chatTriggerMenu.searching": {
|
|
1235
|
+
"defaultMessage": "Searching…",
|
|
1236
|
+
"description": "Shown, and announced politely to screen readers, while a chat composer's @-mention or slash-command menu waits for async search results. Present participle; trailing `…` is one character."
|
|
1237
|
+
},
|
|
1234
1238
|
"@astryx.chatTriggerMenu.suggestions": {
|
|
1235
1239
|
"defaultMessage": "Suggestions",
|
|
1236
1240
|
"description": "Screen-reader-only fallback label for the popover listbox opened by a trigger character (`@`, `/`) in the chat composer when the trigger provides no specific label."
|
package/locales/pseudo.json
CHANGED
|
@@ -923,6 +923,9 @@
|
|
|
923
923
|
"@astryx.chatToolCalls.groupLabel": {
|
|
924
924
|
"defaultMessage": "⟦{count} ţóół çàłłš⟧"
|
|
925
925
|
},
|
|
926
|
+
"@astryx.chatTriggerMenu.searching": {
|
|
927
|
+
"defaultMessage": "⟦Šéàřçĥíñĝ…⟧"
|
|
928
|
+
},
|
|
926
929
|
"@astryx.chatTriggerMenu.suggestions": {
|
|
927
930
|
"defaultMessage": "⟦Šúĝĝéšţíóñš⟧"
|
|
928
931
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4-canary.3f6ea7d",
|
|
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,6 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.4-canary.3f6ea7d",
|
|
796
|
+
"@astryxdesign/cli": "0.7.4-canary.3f6ea7d",
|
|
795
797
|
"@babel/cli": "^7.29.7",
|
|
796
798
|
"@babel/core": "^7.29.7",
|
|
797
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -802,9 +804,7 @@
|
|
|
802
804
|
"@testing-library/jest-dom": "^6.6.0",
|
|
803
805
|
"@testing-library/react": "^16.3.2",
|
|
804
806
|
"@types/babel__core": "^7.20.5",
|
|
805
|
-
"tailwindcss": "^4.3.2"
|
|
806
|
-
"@astryxdesign/a11y-spec": "0.0.0",
|
|
807
|
-
"@astryxdesign/cli": "0.7.3"
|
|
807
|
+
"tailwindcss": "^4.3.2"
|
|
808
808
|
},
|
|
809
809
|
"dependencies": {
|
|
810
810
|
"intl-messageformat": "^11.2.9"
|
|
@@ -95,7 +95,7 @@ export const docs = {
|
|
|
95
95
|
derived: [{property: 'padding', expand: 'container'}],
|
|
96
96
|
},
|
|
97
97
|
description:
|
|
98
|
-
"A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, optional drag-to-resize snap points, and purpose-controlled dismissal. Like Dialog, it is a padded container: content starts at the theme's bottom-sheet padding (--spacing-4 by default), and a lone Section child or bleed children such as Table and Divider align against it. A
|
|
98
|
+
"A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, optional drag-to-resize snap points, and purpose-controlled dismissal. Like Dialog, it is a padded container: content starts at the theme's bottom-sheet padding (--spacing-4 by default), and a lone Section child or bleed children such as Table and Divider align against it. A sheet always owns a native <dialog>; inside BottomSheetSwitcher the switcher presents and closes that dialog as the flow moves between steps. In both modes, ref and shared DOM props target the visual panel <div>.",
|
|
99
99
|
props: [
|
|
100
100
|
{
|
|
101
101
|
name: 'isOpen',
|
|
@@ -165,7 +165,7 @@ export const docs = {
|
|
|
165
165
|
name: 'hasScrim',
|
|
166
166
|
type: 'boolean',
|
|
167
167
|
description:
|
|
168
|
-
"For a standalone BottomSheet, whether to render a scrim, the semi-transparent overlay that covers and blocks the background. true (default) uses showModal(): top layer, focus
|
|
168
|
+
"For a standalone BottomSheet, whether to render a scrim, the semi-transparent overlay that covers and blocks the background. true (default) uses showModal(): top layer, native focus containment, ::backdrop scrim, body scroll lock, and tap-scrim-to-dismiss when purpose='info', with the background inert. false uses show() with no scrim, leaving the page behind interactive and scrollable. For a multi-step flow, configure hasScrim on BottomSheetSwitcher instead; it applies to every child's dialog.",
|
|
169
169
|
default: 'true',
|
|
170
170
|
},
|
|
171
171
|
],
|
|
@@ -66,9 +66,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
66
66
|
`architecture:container-padding`.
|
|
67
67
|
- Standalone sheet presentation, including its optional native-dialog Scrim.
|
|
68
68
|
- The native dialog every sheet renders, standalone or switcher-managed
|
|
69
|
-
(`component:BottomSheetSwitcher` DEC-1).
|
|
70
|
-
switcher-managed sheet renders a panel inside the switcher's one shared
|
|
71
|
-
dialog and owns no dialog of its own.
|
|
69
|
+
(`component:BottomSheetSwitcher` DEC-1).
|
|
72
70
|
|
|
73
71
|
**Does not own / non-goals**
|
|
74
72
|
|
|
@@ -107,8 +105,7 @@ in `BottomSheet.doc.mjs`.
|
|
|
107
105
|
without a Scrim.
|
|
108
106
|
- A switcher-managed sheet renders the same panel anatomy in its own dialog while
|
|
109
107
|
BottomSheetSwitcher coordinates that dialog's lifecycle and the flow's one Scrim
|
|
110
|
-
(`component:BottomSheetSwitcher` DEC-1).
|
|
111
|
-
renders inside the switcher's one shared dialog.
|
|
108
|
+
(`component:BottomSheetSwitcher` DEC-1).
|
|
112
109
|
|
|
113
110
|
### Transformation and precedence order
|
|
114
111
|
|
|
@@ -10,10 +10,22 @@
|
|
|
10
10
|
*
|
|
11
11
|
* BottomSheet selects one of two focused hosts. A standalone host owns its
|
|
12
12
|
* native dialog lifecycle and joins the shared layer dismissal stack; a
|
|
13
|
-
* switcher item
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* switcher item owns a native dialog of its own too, but hands its lifecycle
|
|
14
|
+
* to the parent's transition state machine, scrim ownership, and dismissal
|
|
15
|
+
* registration. Both render the same BottomSheetPanel, which owns sheet
|
|
16
|
+
* presentation, content padding, gestures, mobile-keyboard accommodation,
|
|
17
|
+
* and motion completion.
|
|
18
|
+
*
|
|
19
|
+
* Every sheet dialog is native first. A scrim-backed sheet is presented with
|
|
20
|
+
* showModal(): the browser makes the page inert and keeps focus inside the
|
|
21
|
+
* dialog, delivers Escape and the platform close as `cancel`, paints the
|
|
22
|
+
* scrim as `::backdrop`, and returns focus to the opener on close(). Page
|
|
23
|
+
* scroll is locked by the shared useScrollLock hook, as for Dialog: a
|
|
24
|
+
* standalone sheet holds it while presented, a switcher holds one for the
|
|
25
|
+
* whole modal flow. The remaining script is what no single dialog can
|
|
26
|
+
* decide: gating `cancel` by `purpose` and the shared dismissal stack, and,
|
|
27
|
+
* in a switcher, handing one scrim between two dialogs and keeping focus in
|
|
28
|
+
* the active sheet when a retained dialog closes.
|
|
17
29
|
*
|
|
18
30
|
* SYNC: When modified, update these files to stay in sync:
|
|
19
31
|
* - /packages/core/src/BottomSheet/BottomSheetPanel.tsx
|
|
@@ -44,6 +56,7 @@ import type {DialogPurpose} from '../Dialog';
|
|
|
44
56
|
import type {SpacingStep} from '../utils/types';
|
|
45
57
|
import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
|
|
46
58
|
import {useDevWarning, useScrollLock} from '../hooks';
|
|
59
|
+
import {mergeProps} from '../utils';
|
|
47
60
|
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
|
|
48
61
|
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
49
62
|
import {focusForLastInput} from '../utils/focusReturn';
|
|
@@ -97,14 +110,31 @@ const styles = stylex.create({
|
|
|
97
110
|
'@starting-style': 0,
|
|
98
111
|
},
|
|
99
112
|
transitionProperty: 'opacity, display',
|
|
100
|
-
|
|
113
|
+
// A switcher hands its one scrim from a fading dialog to the next by
|
|
114
|
+
// pinning this duration for the swap; a standalone sheet never sets it.
|
|
115
|
+
transitionDuration: `var(--_sheet-scrim-duration, ${durationVars['--duration-medium']})`,
|
|
101
116
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
102
117
|
transitionBehavior: 'allow-discrete',
|
|
103
118
|
'@media (prefers-reduced-motion: reduce)': {
|
|
104
|
-
|
|
119
|
+
// The switcher's scrim takeover pins the duration to 0s for one style
|
|
120
|
+
// flush; a literal here would override that pin and paint a frame
|
|
121
|
+
// with no scrim under reduced motion.
|
|
122
|
+
transitionDuration: 'var(--_sheet-scrim-duration, 0.01s)',
|
|
105
123
|
},
|
|
106
124
|
},
|
|
107
125
|
},
|
|
126
|
+
// A non-modal switcher dialog does not enter the top layer, so DOM order
|
|
127
|
+
// would decide which sheet paints in front during a handoff. The switcher's
|
|
128
|
+
// flow wrapper is the isolated surface for that relationship; inside it the
|
|
129
|
+
// sheet dialogs use local values only, the interactive sheet's on top.
|
|
130
|
+
dialogNonModalItem: {
|
|
131
|
+
pointerEvents: 'none',
|
|
132
|
+
width: '100%',
|
|
133
|
+
height: '100%',
|
|
134
|
+
},
|
|
135
|
+
dialogNonModalItemTop: {
|
|
136
|
+
zIndex: 1,
|
|
137
|
+
},
|
|
108
138
|
/**
|
|
109
139
|
* The dim leaves with the sheet, on a curve that matches.
|
|
110
140
|
*
|
|
@@ -235,6 +265,43 @@ function focusPanel(panel: HTMLElement | null, isModal: boolean): void {
|
|
|
235
265
|
}
|
|
236
266
|
}
|
|
237
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Present a switcher step's dialog modally with no focus to return to.
|
|
270
|
+
* showModal() records the element focused at that moment and close() moves
|
|
271
|
+
* focus back to it, in Chromium and WebKit even when focus is in another
|
|
272
|
+
* dialog by then. For a step that element is the control that advanced the
|
|
273
|
+
* flow (Continue, Back) or the flow's opener, and the dialog closes a fade
|
|
274
|
+
* later, while the user is in the next step: the return would pull focus
|
|
275
|
+
* off the active panel with no user action. Blurring first leaves nothing
|
|
276
|
+
* recorded, so the close moves nothing; the entering step then focuses its
|
|
277
|
+
* own panel, and the switcher returns focus to the opener itself when the
|
|
278
|
+
* flow ends. The blur happens in the same task as the present, so no frame
|
|
279
|
+
* paints without focus.
|
|
280
|
+
*/
|
|
281
|
+
function presentStepModally(dialog: HTMLDialogElement): void {
|
|
282
|
+
const focused = dialog.ownerDocument.activeElement;
|
|
283
|
+
if (focused instanceof HTMLElement && focused !== dialog.ownerDocument.body) {
|
|
284
|
+
focused.blur();
|
|
285
|
+
}
|
|
286
|
+
dialog.showModal();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Present a switcher dialog whose backdrop must start, and stay, clear: the
|
|
291
|
+
* opacity is set with the scrim transition pinned to 0s, the dialog is
|
|
292
|
+
* presented, and the pin is lifted only after a style flush has applied the
|
|
293
|
+
* clear backdrop, so no frame eases from a previous backdrop value.
|
|
294
|
+
*/
|
|
295
|
+
function presentWithClearScrim(dialog: HTMLDialogElement): void {
|
|
296
|
+
dialog.style.setProperty('--_sheet-scrim-duration', '0s');
|
|
297
|
+
dialog.style.setProperty('--_sheet-scrim-opacity', '0');
|
|
298
|
+
presentStepModally(dialog);
|
|
299
|
+
if (typeof getComputedStyle === 'function') {
|
|
300
|
+
void getComputedStyle(dialog, '::backdrop').opacity;
|
|
301
|
+
}
|
|
302
|
+
dialog.style.removeProperty('--_sheet-scrim-duration');
|
|
303
|
+
}
|
|
304
|
+
|
|
238
305
|
function StandaloneBottomSheet({
|
|
239
306
|
ref,
|
|
240
307
|
isOpen,
|
|
@@ -449,12 +516,21 @@ function SwitcherBottomSheetItem({
|
|
|
449
516
|
onActiveSheetChange,
|
|
450
517
|
getSheetPhase,
|
|
451
518
|
getSheetAlignmentOffset,
|
|
519
|
+
hostSheet,
|
|
520
|
+
isScrimOwner,
|
|
521
|
+
isFlowClosing,
|
|
452
522
|
registerSheetElement,
|
|
453
|
-
|
|
523
|
+
registerSheetDialog,
|
|
454
524
|
registerSheetPurpose,
|
|
525
|
+
onBeforeSheetPresent,
|
|
526
|
+
isCoveredInTopLayer,
|
|
455
527
|
onSheetEnterStart,
|
|
456
528
|
onSheetTransitionComplete,
|
|
457
529
|
onSheetScrimOpacityChange,
|
|
530
|
+
dialogProps,
|
|
531
|
+
onDialogCancel,
|
|
532
|
+
onDialogClick,
|
|
533
|
+
onDialogKeyDown,
|
|
458
534
|
} = switcher;
|
|
459
535
|
const hasValidSheetId = typeof sheetId === 'string' && sheetId.length > 0;
|
|
460
536
|
const phase = hasValidSheetId ? getSheetPhase(sheetId) : 'hidden';
|
|
@@ -470,6 +546,10 @@ function SwitcherBottomSheetItem({
|
|
|
470
546
|
phase === 'exiting';
|
|
471
547
|
const isPresented = phase !== 'hidden';
|
|
472
548
|
const isTopSheet = phase === 'active' || phase === 'entering';
|
|
549
|
+
const ownsScrim = hasValidSheetId && isScrimOwner(sheetId);
|
|
550
|
+
const isHostSheet = hasValidSheetId && hostSheet === sheetId;
|
|
551
|
+
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
|
552
|
+
const dialogModeRef = useRef<'modal' | 'non-modal' | null>(null);
|
|
473
553
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
474
554
|
const previousPhaseRef = useRef(phase);
|
|
475
555
|
const previousPhase = previousPhaseRef.current;
|
|
@@ -493,6 +573,15 @@ function SwitcherBottomSheetItem({
|
|
|
493
573
|
},
|
|
494
574
|
[hasValidSheetId, registerSheetElement, sheetId],
|
|
495
575
|
);
|
|
576
|
+
const handleDialogElementChange = useCallback(
|
|
577
|
+
(element: HTMLDialogElement | null) => {
|
|
578
|
+
dialogRef.current = element;
|
|
579
|
+
if (hasValidSheetId) {
|
|
580
|
+
registerSheetDialog(sheetId, element);
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
[hasValidSheetId, registerSheetDialog, sheetId],
|
|
584
|
+
);
|
|
496
585
|
const handleMotionStart = useCallback(
|
|
497
586
|
(motion: BottomSheetPanelMotion) => {
|
|
498
587
|
if (motion === 'entering' && hasValidSheetId) {
|
|
@@ -522,17 +611,90 @@ function SwitcherBottomSheetItem({
|
|
|
522
611
|
if (!hasValidSheetId) {
|
|
523
612
|
return;
|
|
524
613
|
}
|
|
525
|
-
|
|
526
|
-
return () =>
|
|
527
|
-
}, [hasValidSheetId,
|
|
614
|
+
registerSheetPurpose(sheetId, purpose);
|
|
615
|
+
return () => registerSheetPurpose(sheetId, null);
|
|
616
|
+
}, [hasValidSheetId, purpose, registerSheetPurpose, sheetId]);
|
|
528
617
|
|
|
618
|
+
// Present this sheet's own dialog while the switcher keeps the sheet on
|
|
619
|
+
// screen, and close it once the sheet is hidden. A scrim flow presents
|
|
620
|
+
// modally: an entering dialog is shown above the retained one in the top
|
|
621
|
+
// layer, and the retained dialog closes only after its panel has faded. A
|
|
622
|
+
// modal dialog that is still open beneath a newer one -- the user went back
|
|
623
|
+
// to a sheet that was still fading -- re-presents to become topmost again:
|
|
624
|
+
// the document is blocked by the topmost modal dialog, so a lower one is
|
|
625
|
+
// inert whatever the switcher calls it.
|
|
626
|
+
//
|
|
627
|
+
// Every present blurs first, so no step dialog records an element to
|
|
628
|
+
// return focus to: a close while another sheet is active moves nothing,
|
|
629
|
+
// and when the flow ends the switcher focuses the flow's opener itself.
|
|
529
630
|
useLayoutEffect(() => {
|
|
530
|
-
|
|
631
|
+
const dialog = dialogRef.current;
|
|
632
|
+
if (dialog == null || !hasValidSheetId) {
|
|
531
633
|
return;
|
|
532
634
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
635
|
+
if (!isPresented) {
|
|
636
|
+
if (dialog.open) {
|
|
637
|
+
dialog.close();
|
|
638
|
+
}
|
|
639
|
+
dialogModeRef.current = null;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const nextMode = hasScrim ? 'modal' : 'non-modal';
|
|
643
|
+
if (dialog.open && dialogModeRef.current !== nextMode) {
|
|
644
|
+
dialog.close();
|
|
645
|
+
}
|
|
646
|
+
if (
|
|
647
|
+
dialog.open &&
|
|
648
|
+
hasScrim &&
|
|
649
|
+
isInteractive &&
|
|
650
|
+
isCoveredInTopLayer(sheetId)
|
|
651
|
+
) {
|
|
652
|
+
dialog.close();
|
|
653
|
+
}
|
|
654
|
+
if (!dialog.open) {
|
|
655
|
+
if (hasScrim) {
|
|
656
|
+
onBeforeSheetPresent(sheetId);
|
|
657
|
+
if (ownsScrim) {
|
|
658
|
+
presentStepModally(dialog);
|
|
659
|
+
} else {
|
|
660
|
+
// Entering above a retained dialog whose backdrop already dims the
|
|
661
|
+
// page: present with a clear backdrop so the page is not dimmed
|
|
662
|
+
// twice and the previous panel stays undimmed beneath this one.
|
|
663
|
+
// Pinned to 0s across the present: a dialog re-presented from the
|
|
664
|
+
// retained position (the user went back while it was fading)
|
|
665
|
+
// closed with its backdrop at 1, and would otherwise ease it down
|
|
666
|
+
// over the scrim duration on top of the new owner's backdrop.
|
|
667
|
+
presentWithClearScrim(dialog);
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
dialog.show();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
dialogModeRef.current = nextMode;
|
|
674
|
+
}, [
|
|
675
|
+
hasScrim,
|
|
676
|
+
hasValidSheetId,
|
|
677
|
+
isCoveredInTopLayer,
|
|
678
|
+
isInteractive,
|
|
679
|
+
isPresented,
|
|
680
|
+
onBeforeSheetPresent,
|
|
681
|
+
ownsScrim,
|
|
682
|
+
sheetId,
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
// A sheet removed while presented -- a consumer unmounts the closing sheet
|
|
686
|
+
// in the same update that closes the flow -- takes its dialog out of the
|
|
687
|
+
// top layer with it, but the element would keep its `open` state. Close it
|
|
688
|
+
// while it is still attached so the native lifecycle ends cleanly.
|
|
689
|
+
useLayoutEffect(
|
|
690
|
+
() => () => {
|
|
691
|
+
const dialog = dialogRef.current;
|
|
692
|
+
if (dialog?.open) {
|
|
693
|
+
dialog.close();
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
[],
|
|
697
|
+
);
|
|
536
698
|
|
|
537
699
|
useEffect(() => {
|
|
538
700
|
if (isInteractive) {
|
|
@@ -554,45 +716,96 @@ function SwitcherBottomSheetItem({
|
|
|
554
716
|
isInteractive && !label,
|
|
555
717
|
);
|
|
556
718
|
|
|
719
|
+
// The switcher's dialog surface applies to every sheet dialog, except the
|
|
720
|
+
// attributes that identify one element: those render on the host dialog
|
|
721
|
+
// only, the one the switcher's ref resolves to. Of the attributes set
|
|
722
|
+
// explicitly below, only `role` has a consumer value to keep: a required
|
|
723
|
+
// step forces `alertdialog`, otherwise the switcher's `role` stands, as on
|
|
724
|
+
// a shared dialog. `aria-label`/`aria-labelledby` keep consumer precedence
|
|
725
|
+
// above; `aria-modal`, `aria-hidden`, and `inert` are the host's own state.
|
|
726
|
+
const {
|
|
727
|
+
className: switcherClassName,
|
|
728
|
+
style: switcherStyle,
|
|
729
|
+
xstyle: switcherXstyle,
|
|
730
|
+
'aria-label': switcherAriaLabel,
|
|
731
|
+
'aria-labelledby': switcherAriaLabelledBy,
|
|
732
|
+
id: switcherId,
|
|
733
|
+
'data-testid': switcherTestId,
|
|
734
|
+
...switcherDialogProps
|
|
735
|
+
} = dialogProps;
|
|
736
|
+
const dialogPresentationProps = mergeProps(
|
|
737
|
+
stylex.props(
|
|
738
|
+
styles.dialog,
|
|
739
|
+
isPresented && styles.dialogOpen,
|
|
740
|
+
hasScrim && styles.scrim,
|
|
741
|
+
hasScrim && ownsScrim && isFlowClosing && styles.scrimClosing,
|
|
742
|
+
!hasScrim && styles.dialogNonModalItem,
|
|
743
|
+
!hasScrim && isTopSheet && styles.dialogNonModalItemTop,
|
|
744
|
+
switcherXstyle,
|
|
745
|
+
),
|
|
746
|
+
switcherClassName,
|
|
747
|
+
switcherStyle,
|
|
748
|
+
);
|
|
749
|
+
|
|
557
750
|
return (
|
|
558
|
-
<
|
|
559
|
-
{...
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
751
|
+
<dialog
|
|
752
|
+
{...switcherDialogProps}
|
|
753
|
+
{...dialogPresentationProps}
|
|
754
|
+
id={isHostSheet ? switcherId : undefined}
|
|
755
|
+
data-testid={isHostSheet ? switcherTestId : undefined}
|
|
756
|
+
ref={handleDialogElementChange}
|
|
757
|
+
aria-label={
|
|
758
|
+
switcherAriaLabel ??
|
|
759
|
+
(switcherAriaLabelledBy == null ? label : undefined)
|
|
760
|
+
}
|
|
761
|
+
aria-labelledby={switcherAriaLabelledBy}
|
|
762
|
+
aria-modal={hasScrim && isInteractive ? 'true' : undefined}
|
|
565
763
|
aria-hidden={isInactive ? 'true' : undefined}
|
|
566
|
-
inert={isInactive ? true : undefined}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
{
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
764
|
+
inert={isInactive ? true : undefined}
|
|
765
|
+
role={purpose === 'required' ? 'alertdialog' : switcherDialogProps.role}
|
|
766
|
+
onCancel={onDialogCancel}
|
|
767
|
+
onClick={onDialogClick}
|
|
768
|
+
onKeyDown={onDialogKeyDown}>
|
|
769
|
+
<div
|
|
770
|
+
{...stylex.props(
|
|
771
|
+
styles.positioner,
|
|
772
|
+
!isPresented && styles.positionerHidden,
|
|
773
|
+
isTopSheet && styles.positionerTop,
|
|
774
|
+
)}
|
|
775
|
+
hidden={!isPresented}
|
|
776
|
+
aria-hidden={isInactive ? 'true' : undefined}
|
|
777
|
+
inert={isInactive ? true : undefined}>
|
|
778
|
+
<BottomSheetPanel
|
|
779
|
+
{...props}
|
|
780
|
+
ref={ref}
|
|
781
|
+
state={panelState}
|
|
782
|
+
height={height}
|
|
783
|
+
label={label}
|
|
784
|
+
snapPoints={snapPoints}
|
|
785
|
+
isSwipeDismissAllowed={purpose === 'info'}
|
|
786
|
+
isPageScrollLocked={hasScrim}
|
|
787
|
+
xstyle={xstyle}
|
|
788
|
+
onDismiss={dismissOnSwipe}
|
|
789
|
+
onScrimOpacity={handleScrimOpacity}
|
|
790
|
+
onElementChange={handlePanelElementChange}
|
|
791
|
+
onMotionStart={handleMotionStart}
|
|
792
|
+
onMotionComplete={handleMotionComplete}>
|
|
793
|
+
{/* A switcher item consumes its parent controller. Its content starts a
|
|
794
|
+
fresh ownership scope so a nested BottomSheet is standalone unless
|
|
795
|
+
it establishes a nested BottomSheetSwitcher of its own. */}
|
|
796
|
+
<BottomSheetSwitcherContext value={null}>
|
|
797
|
+
<LayerDepthProvider>{children}</LayerDepthProvider>
|
|
798
|
+
</BottomSheetSwitcherContext>
|
|
799
|
+
</BottomSheetPanel>
|
|
800
|
+
</div>
|
|
801
|
+
{isPresented && <BottomSheetEdgeTint />}
|
|
802
|
+
</dialog>
|
|
590
803
|
);
|
|
591
804
|
}
|
|
592
805
|
|
|
593
806
|
/**
|
|
594
|
-
* A mobile touch sheet that
|
|
595
|
-
* BottomSheetSwitcher
|
|
807
|
+
* A mobile touch sheet that owns a native dialog. Given a sheetId inside a
|
|
808
|
+
* BottomSheetSwitcher, that switcher coordinates the dialog's lifecycle.
|
|
596
809
|
*/
|
|
597
810
|
export function BottomSheet(props: BottomSheetProps) {
|
|
598
811
|
const switcher = use(BottomSheetSwitcherContext);
|
|
@@ -127,8 +127,10 @@ describe('BottomSheetEdgeTint', () => {
|
|
|
127
127
|
expect(tints()).toHaveLength(0);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
130
|
+
// Each switcher step owns a dialog and tints it while presented: one tint
|
|
131
|
+
// at rest, one per open dialog through a handoff (the retained dialog's
|
|
132
|
+
// sits beneath the entering dialog), none for a sheet that is not
|
|
133
|
+
// presented.
|
|
132
134
|
it('gives a non-modal switcher flow exactly one tint', () => {
|
|
133
135
|
render(
|
|
134
136
|
<BottomSheetSwitcher
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
const anatomy = [
|
|
5
5
|
{
|
|
6
|
-
name: '
|
|
6
|
+
name: 'Sheet dialogs',
|
|
7
7
|
required: true,
|
|
8
8
|
description:
|
|
9
|
-
|
|
9
|
+
"One native dialog per child sheet, presented and closed by the switcher; the dialog hosting the active sheet carries the flow's ref, id, modality, and dismissal.",
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
name: 'Sheet panels',
|
|
@@ -18,7 +18,7 @@ const anatomy = [
|
|
|
18
18
|
name: 'Scrim',
|
|
19
19
|
required: false,
|
|
20
20
|
description:
|
|
21
|
-
'Native dialog backdrop shown by the default scrim-backed modal presentation.',
|
|
21
|
+
'Native dialog backdrop shown by the default scrim-backed modal presentation; one sheet dialog paints it at a time, so a handoff never dims the page twice.',
|
|
22
22
|
},
|
|
23
23
|
];
|
|
24
24
|
|
|
@@ -142,18 +142,19 @@ export const docs = {
|
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
description:
|
|
145
|
-
'Coordinates multiple BottomSheets as a mutually exclusive flow. One activeSheet ID selects the only interactive sheet; during a handoff, the new sheet enters above the inert previous sheet. If the new sheet is shorter, the previous sheet simultaneously moves down until their top edges align, then fades after both transforms complete.
|
|
145
|
+
'Coordinates multiple BottomSheets as a mutually exclusive flow. One activeSheet ID selects the only interactive sheet; during a handoff, the new sheet enters above the inert previous sheet. If the new sheet is shorter, the previous sheet simultaneously moves down until their top edges align, then fades after both transforms complete. Each child sheet renders its own native <dialog>, exactly like a standalone sheet: modal flows present each sheet with showModal(), no-scrim flows with show(). The browser supplies modality: the page is inert and focus stays in the top-layer dialog, so both hold across a handoff; the switcher holds the shared body scroll lock once for the whole modal flow. The switcher coordinates the rest: one scrim paints at a time, focus returns to the element that opened the flow, its ref, id, and data-testid resolve to the dialog hosting the active sheet, and its other dialog props and handlers apply to every sheet dialog.',
|
|
146
146
|
props: [
|
|
147
147
|
{
|
|
148
148
|
name: 'ref',
|
|
149
149
|
type: 'Ref<HTMLDialogElement>',
|
|
150
|
-
description:
|
|
150
|
+
description:
|
|
151
|
+
"Ref resolved to the native dialog hosting the flow: the active sheet's dialog, which changes to the next sheet's on a handoff; the exiting sheet's through its exit; and, while the flow is closed, the dialog that hosted it last (or the first mounted sheet's). It is never null while a sheet is mounted, from the first commit on. id and data-testid render on this dialog only: a flow that mounts open carries them on the active sheet's dialog in server markup too; a flow that mounts closed carries them from the first commit's synchronous re-render on, and its server markup carries them on no element. Other dialog props apply to every sheet dialog.",
|
|
151
152
|
},
|
|
152
153
|
{
|
|
153
154
|
name: 'onCancel',
|
|
154
155
|
type: '(event: SyntheticEvent<HTMLDialogElement>) => void',
|
|
155
156
|
description:
|
|
156
|
-
|
|
157
|
+
"Called before the switcher handles a native cancel request (Escape or the platform close). Attached to every sheet dialog; the browser raises cancel on the dialog it is presenting, the active sheet's. Calling preventDefault() keeps the controlled flow open.",
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
160
|
name: 'activeSheet',
|
|
@@ -173,7 +174,7 @@ export const docs = {
|
|
|
173
174
|
name: 'hasScrim',
|
|
174
175
|
type: 'boolean',
|
|
175
176
|
description:
|
|
176
|
-
"Whether the
|
|
177
|
+
"Whether the sheet dialogs are modal. true presents each sheet with showModal() for a native ::backdrop, native focus containment, one body scroll lock held for the whole flow, and click-to-dismiss when the active BottomSheet has purpose='info'; only one backdrop paints at a time. false uses show() with no backdrop and leaves the page interactive; avoid transformed, contained, or clipping ancestors because a non-modal dialog remains in its containing context.",
|
|
177
178
|
default: 'true',
|
|
178
179
|
},
|
|
179
180
|
{
|
|
@@ -186,7 +187,7 @@ export const docs = {
|
|
|
186
187
|
usage: {
|
|
187
188
|
anatomy,
|
|
188
189
|
description:
|
|
189
|
-
"Coordinates a multi-step bottom-sheet flow
|
|
190
|
+
"Coordinates a multi-step bottom-sheet flow, one native dialog per sheet; set activeSheet to a nested BottomSheet's sheetId to open or switch steps, and to null to close.",
|
|
190
191
|
bestPractices: [
|
|
191
192
|
{
|
|
192
193
|
guidance: true,
|
|
@@ -243,11 +244,11 @@ export const docs = {
|
|
|
243
244
|
/** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
|
|
244
245
|
export const docsDense = {
|
|
245
246
|
description:
|
|
246
|
-
'controller
|
|
247
|
+
'controller for mutually exclusive multi-step BottomSheets, one native dialog per sheet',
|
|
247
248
|
usage: {
|
|
248
249
|
anatomy,
|
|
249
250
|
description:
|
|
250
|
-
"Coordinates a multi-step bottom-sheet flow
|
|
251
|
+
"Coordinates a multi-step bottom-sheet flow, one native dialog per sheet; set activeSheet to a nested BottomSheet's sheetId to open or switch steps, and to null to close.",
|
|
251
252
|
bestPractices: [
|
|
252
253
|
{
|
|
253
254
|
guidance: true,
|