@astryxdesign/core 0.7.3 → 0.7.4-canary.b25f95e
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/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/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/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/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/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 +369 -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/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
|
@@ -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,
|