@astryxdesign/core 0.7.7-canary.4931743 → 0.7.7-canary.4e495ec
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.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +76 -6
- package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
- package/dist/BottomSheet/bottomSheetStack.js +5 -4
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.tsx +85 -5
- package/src/BottomSheet/bottomSheetStack.ts +8 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAuBH,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAuBH,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAwDhD,YAAY,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAmLhF,UAAU,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,qDAAqD;IACrD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IAEd,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB,wGAAwG;IACxG,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAE7C;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAEjD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,UAAU,0BAA2B,SAAQ,sBAAsB;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,UAAU,wBAAyB,SAAQ,sBAAsB;IAC/D;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GAC1B,0BAA0B,GAAG,wBAAwB,CAAC;AAo5BxD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA6BlD;yBA7Be,WAAW"}
|
|
@@ -58,6 +58,7 @@ import { colorVars, durationVars, easeVars, spacingVars } from "../theme/tokens.
|
|
|
58
58
|
import { useDevWarning } from "../hooks/useDevWarning.js";
|
|
59
59
|
import { useScrollLock } from "../hooks/useScrollLock.js";
|
|
60
60
|
import { useMergedRefs } from "../hooks/useMergedRefs.js";
|
|
61
|
+
import { observeMutations } from "../utils/sharedMutationObserver.js";
|
|
61
62
|
import { mergeProps } from "../utils/mergeProps.js";
|
|
62
63
|
import { LayerDepthProvider } from "../Layer/LayerDepthContext.js";
|
|
63
64
|
import { ModalAnnounceProvider } from "../hooks/ModalAnnounceProvider.js";
|
|
@@ -135,6 +136,10 @@ const styles = {
|
|
|
135
136
|
kb4ib: "x1cz9k3x",
|
|
136
137
|
$$css: true
|
|
137
138
|
},
|
|
139
|
+
exitHoldingFocus: {
|
|
140
|
+
kfzvcC: "x47corl",
|
|
141
|
+
$$css: true
|
|
142
|
+
},
|
|
138
143
|
positioner: {
|
|
139
144
|
kVAEAm: "x10l6tqk",
|
|
140
145
|
kkqhue: "x17y0mx6",
|
|
@@ -176,6 +181,23 @@ function stackPositionerStyles(view) {
|
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
/** The `data-sheet-stack` value for a host: only set while sheets stack. */
|
|
184
|
+
/** Stops a click, from any input, before it reaches its target. */
|
|
185
|
+
function ignoreActivation(event) {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Stops every key except Tab before it reaches the focused control, so no
|
|
192
|
+
* key handler runs and no text changes (a search field's Enter, a typed
|
|
193
|
+
* letter, Backspace, an arrow that reopens a list). Tab still moves focus.
|
|
194
|
+
*/
|
|
195
|
+
function ignoreKeyDuringExit(event) {
|
|
196
|
+
if (event.key !== 'Tab') {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
179
201
|
function stackStateAttribute(view, isOpen) {
|
|
180
202
|
if (!view.isStacked) {
|
|
181
203
|
return undefined;
|
|
@@ -291,10 +313,14 @@ function StandaloneBottomSheet({
|
|
|
291
313
|
// `isOpen` restarts the flag during render, and the layout effect below
|
|
292
314
|
// settles a close.
|
|
293
315
|
const [isCloseSettled, setIsCloseSettled] = useState(false);
|
|
316
|
+
// Whether focus was inside the sheet when it closed. Such a sheet stays out
|
|
317
|
+
// of `inert` and `aria-hidden` until its exit ends and focus has left.
|
|
318
|
+
const [isExitFocusHeld, setIsExitFocusHeld] = useState(false);
|
|
294
319
|
const [settledIsOpen, setSettledIsOpen] = useState(isOpen);
|
|
295
320
|
if (settledIsOpen !== isOpen) {
|
|
296
321
|
setSettledIsOpen(isOpen);
|
|
297
322
|
setIsCloseSettled(false);
|
|
323
|
+
setIsExitFocusHeld(false);
|
|
298
324
|
}
|
|
299
325
|
const panelState = isOpen ? {
|
|
300
326
|
kind: 'open',
|
|
@@ -408,9 +434,8 @@ function StandaloneBottomSheet({
|
|
|
408
434
|
}, [hasScrim, shouldPresent, writeScrim]);
|
|
409
435
|
useLayoutEffect(() => {
|
|
410
436
|
if (!isOpen) {
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// focus was when the person closed the sheet.
|
|
437
|
+
// Where focus was when the person closed the sheet. The closing sheet
|
|
438
|
+
// is not inert yet, so focus has not moved.
|
|
414
439
|
const dialog = dialogRef.current;
|
|
415
440
|
hadFocusAtCloseRef.current = dialog != null && dialog.contains(dialog.ownerDocument.activeElement);
|
|
416
441
|
}
|
|
@@ -422,6 +447,8 @@ function StandaloneBottomSheet({
|
|
|
422
447
|
// that was not paired as a replace is an exit from here on.
|
|
423
448
|
// eslint-disable-next-line @eslint-react/set-state-in-effect -- settles the close once the registry has answered
|
|
424
449
|
setIsCloseSettled(true);
|
|
450
|
+
// Read with the close, before the sheet can go inert.
|
|
451
|
+
setIsExitFocusHeld(hadFocusAtCloseRef.current);
|
|
425
452
|
}
|
|
426
453
|
}, [hasScrim, isOpen, shouldPresent]);
|
|
427
454
|
|
|
@@ -507,7 +534,36 @@ function StandaloneBottomSheet({
|
|
|
507
534
|
const {
|
|
508
535
|
ref: modalityRef
|
|
509
536
|
} = useInteractionModalityTracking();
|
|
510
|
-
|
|
537
|
+
// Ends the exit hold once focus has left the sheet: moved elsewhere, or
|
|
538
|
+
// dropped to the page because the focused control was removed or disabled.
|
|
539
|
+
// Native listeners, because React drops the `focusout` a browser sends while
|
|
540
|
+
// React removes the focused element, and Firefox and WebKit send no
|
|
541
|
+
// `focusout` for a removed element at all, so a subtree change is read too.
|
|
542
|
+
// Each reads the focus after the event. They attach only while a closing
|
|
543
|
+
// sheet holds focus, so a closed or open sheet observes nothing.
|
|
544
|
+
const isWatchingExitFocus = !isOpen && isPresented && !isReplaced && isExitFocusHeld;
|
|
545
|
+
const exitFocusRef = useCallback(dialog => {
|
|
546
|
+
if (dialog == null || !isWatchingExitFocus) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const releaseIfFocusLeft = () => {
|
|
550
|
+
if (!dialog.contains(dialog.ownerDocument.activeElement)) {
|
|
551
|
+
setIsExitFocusHeld(false);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const onFocusOut = () => queueMicrotask(releaseIfFocusLeft);
|
|
555
|
+
dialog.addEventListener('focusout', onFocusOut);
|
|
556
|
+
const unobserve = observeMutations(dialog, {
|
|
557
|
+
childList: true,
|
|
558
|
+
subtree: true,
|
|
559
|
+
attributeFilter: ['disabled', 'hidden', 'inert']
|
|
560
|
+
}, releaseIfFocusLeft);
|
|
561
|
+
return () => {
|
|
562
|
+
dialog.removeEventListener('focusout', onFocusOut);
|
|
563
|
+
unobserve();
|
|
564
|
+
};
|
|
565
|
+
}, [isWatchingExitFocus]);
|
|
566
|
+
const dialogComposedRef = useMergedRefs(dialogRef, modalityRef, exitFocusRef);
|
|
511
567
|
useScrollLock(shouldPresent && hasScrim);
|
|
512
568
|
useDevWarning('BottomSheet', 'requires a non-empty `label` for an accessible name; the open sheet ' + 'has no built-in heading to derive one from.', isOpen && !label);
|
|
513
569
|
|
|
@@ -563,13 +619,21 @@ function StandaloneBottomSheet({
|
|
|
563
619
|
// that contains no presented sheet is marked inert and hidden as well, so
|
|
564
620
|
// the state is explicit; one that does contain the top sheet is left alone,
|
|
565
621
|
// because inert on it would reach the top sheet too.
|
|
566
|
-
|
|
622
|
+
// A sheet that closes with focus inside it is neither inert nor hidden
|
|
623
|
+
// while that focus stays: `inert` would drop the focus to the page, and
|
|
624
|
+
// `aria-hidden` would hide the focused control. When the exit ends, the
|
|
625
|
+
// dialog closes and focus returns in the same task. When focus leaves the
|
|
626
|
+
// sheet first (into a sheet opened above it, for example), the sheet goes
|
|
627
|
+
// inert at once. Until the close settles, no closing sheet is inert, so the
|
|
628
|
+
// first commit of a close never hides focus.
|
|
629
|
+
const isExitHoldingFocus = isExiting && !isReplaced && (!isCloseSettled || isExitFocusHeld);
|
|
630
|
+
const isInert = isExiting && !isExitHoldingFocus || stackView.isBlocked;
|
|
567
631
|
return /*#__PURE__*/_jsxs("dialog", {
|
|
568
632
|
...stylex.props(styles.dialog, shouldPresent && styles.dialogOpen, shouldPresent && styles.dialogPanelHost,
|
|
569
633
|
// The dialog is the panel's positioner, so the stack's recede
|
|
570
634
|
// transforms the dialog: panel, grabber and all shrink and lift as
|
|
571
635
|
// one. The backdrop is not a descendant and stays put.
|
|
572
|
-
...stackPositionerStyles(stackView), hasScrim && styles.scrim, hasScrim && isEntrySettled && styles.scrimSettled, hasScrim && isExiting && styles.scrimClosing, !hasScrim && styles.dialogNonModal),
|
|
636
|
+
...stackPositionerStyles(stackView), hasScrim && styles.scrim, hasScrim && isEntrySettled && styles.scrimSettled, hasScrim && isExiting && styles.scrimClosing, isExitHoldingFocus && styles.exitHoldingFocus, !hasScrim && styles.dialogNonModal),
|
|
573
637
|
ref: dialogComposedRef
|
|
574
638
|
// A non-modal sheet is hosted in the top layer as a manual popover
|
|
575
639
|
// (no light dismiss, no backdrop), so sheets order by presentation
|
|
@@ -586,6 +650,12 @@ function StandaloneBottomSheet({
|
|
|
586
650
|
"data-sheet-stack": hasScrim ? stackStateAttribute(stackView, isOpen) : undefined,
|
|
587
651
|
"data-sheet-stack-depth": hasScrim && stackView.depth > 0 ? stackView.depth : undefined,
|
|
588
652
|
onCancel: handleCancel
|
|
653
|
+
// A closing sheet that keeps focus takes no activation during its
|
|
654
|
+
// exit: no click and no key but Tab reaches the control that has focus.
|
|
655
|
+
,
|
|
656
|
+
onClickCapture: isExitHoldingFocus ? ignoreActivation : undefined,
|
|
657
|
+
onKeyDownCapture: isExitHoldingFocus ? ignoreKeyDuringExit : undefined,
|
|
658
|
+
onKeyUpCapture: isExitHoldingFocus ? ignoreKeyDuringExit : undefined
|
|
589
659
|
// Presses inside the sheet do not reach handlers around where it is
|
|
590
660
|
// written. The dialog, not the panel, is the root, as in a switcher.
|
|
591
661
|
,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bottomSheetStack.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/bottomSheetStack.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"bottomSheetStack.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/bottomSheetStack.ts"],"names":[],"mappings":"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"}
|
|
@@ -593,10 +593,11 @@ function openModalsOutside(element) {
|
|
|
593
593
|
if (dialog === element || dialog.contains(element)) {
|
|
594
594
|
continue;
|
|
595
595
|
}
|
|
596
|
-
// A modal on its way out
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
|
|
596
|
+
// A modal on its way out blocks nothing any more: it is gone for this
|
|
597
|
+
// decision. A closing sheet keeps its dialog modal through the exit
|
|
598
|
+
// animation; it is inert and hidden then, unless focus was inside it at
|
|
599
|
+
// the close, so the registry's own record of the close counts too.
|
|
600
|
+
if (dialog.hasAttribute('inert') || dialog.getAttribute('aria-hidden') === 'true' || state.entries.some(entry => !entry.isOpen && entry.getElement() === dialog)) {
|
|
600
601
|
continue;
|
|
601
602
|
}
|
|
602
603
|
if (isModalDialog(dialog)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.7-canary.
|
|
3
|
+
"version": "0.7.7-canary.4e495ec",
|
|
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",
|
|
@@ -865,9 +865,9 @@
|
|
|
865
865
|
"react-dom": ">=19.0.0"
|
|
866
866
|
},
|
|
867
867
|
"devDependencies": {
|
|
868
|
-
"@astryxdesign/a11y-spec": "0.7.7-canary.
|
|
869
|
-
"@astryxdesign/cli": "0.7.7-canary.
|
|
870
|
-
"@astryxdesign/interaction-harness": "0.7.7-canary.
|
|
868
|
+
"@astryxdesign/a11y-spec": "0.7.7-canary.4e495ec",
|
|
869
|
+
"@astryxdesign/cli": "0.7.7-canary.4e495ec",
|
|
870
|
+
"@astryxdesign/interaction-harness": "0.7.7-canary.4e495ec",
|
|
871
871
|
"@babel/cli": "^7.29.7",
|
|
872
872
|
"@babel/core": "^7.29.7",
|
|
873
873
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
76
76
|
import {useScrollLock} from '../hooks/useScrollLock';
|
|
77
77
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
78
|
+
import {observeMutations} from '../utils/sharedMutationObserver';
|
|
78
79
|
import {mergeProps} from '../utils/mergeProps';
|
|
79
80
|
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
|
|
80
81
|
import {ModalAnnounceProvider} from '../hooks/ModalAnnounceProvider';
|
|
@@ -212,6 +213,10 @@ const styles = stylex.create({
|
|
|
212
213
|
transitionTimingFunction: 'linear',
|
|
213
214
|
},
|
|
214
215
|
},
|
|
216
|
+
// A closing sheet that keeps focus takes no pointer input during its exit.
|
|
217
|
+
exitHoldingFocus: {
|
|
218
|
+
pointerEvents: 'none',
|
|
219
|
+
},
|
|
215
220
|
positioner: {
|
|
216
221
|
position: 'absolute',
|
|
217
222
|
insetInline: 0,
|
|
@@ -260,6 +265,24 @@ function stackPositionerStyles(view: BottomSheetStackView) {
|
|
|
260
265
|
}
|
|
261
266
|
|
|
262
267
|
/** The `data-sheet-stack` value for a host: only set while sheets stack. */
|
|
268
|
+
/** Stops a click, from any input, before it reaches its target. */
|
|
269
|
+
function ignoreActivation(event: React.MouseEvent): void {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
event.stopPropagation();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Stops every key except Tab before it reaches the focused control, so no
|
|
276
|
+
* key handler runs and no text changes (a search field's Enter, a typed
|
|
277
|
+
* letter, Backspace, an arrow that reopens a list). Tab still moves focus.
|
|
278
|
+
*/
|
|
279
|
+
function ignoreKeyDuringExit(event: React.KeyboardEvent): void {
|
|
280
|
+
if (event.key !== 'Tab') {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
263
286
|
function stackStateAttribute(
|
|
264
287
|
view: BottomSheetStackView,
|
|
265
288
|
isOpen: boolean,
|
|
@@ -450,10 +473,14 @@ function StandaloneBottomSheet({
|
|
|
450
473
|
// `isOpen` restarts the flag during render, and the layout effect below
|
|
451
474
|
// settles a close.
|
|
452
475
|
const [isCloseSettled, setIsCloseSettled] = useState(false);
|
|
476
|
+
// Whether focus was inside the sheet when it closed. Such a sheet stays out
|
|
477
|
+
// of `inert` and `aria-hidden` until its exit ends and focus has left.
|
|
478
|
+
const [isExitFocusHeld, setIsExitFocusHeld] = useState(false);
|
|
453
479
|
const [settledIsOpen, setSettledIsOpen] = useState(isOpen);
|
|
454
480
|
if (settledIsOpen !== isOpen) {
|
|
455
481
|
setSettledIsOpen(isOpen);
|
|
456
482
|
setIsCloseSettled(false);
|
|
483
|
+
setIsExitFocusHeld(false);
|
|
457
484
|
}
|
|
458
485
|
const panelState: BottomSheetPanelState = isOpen
|
|
459
486
|
? {kind: 'open', entering: stackView.isReplacing}
|
|
@@ -581,9 +608,8 @@ function StandaloneBottomSheet({
|
|
|
581
608
|
}, [hasScrim, shouldPresent, writeScrim]);
|
|
582
609
|
useLayoutEffect(() => {
|
|
583
610
|
if (!isOpen) {
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
// focus was when the person closed the sheet.
|
|
611
|
+
// Where focus was when the person closed the sheet. The closing sheet
|
|
612
|
+
// is not inert yet, so focus has not moved.
|
|
587
613
|
const dialog = dialogRef.current;
|
|
588
614
|
hadFocusAtCloseRef.current =
|
|
589
615
|
dialog != null && dialog.contains(dialog.ownerDocument.activeElement);
|
|
@@ -596,6 +622,8 @@ function StandaloneBottomSheet({
|
|
|
596
622
|
// that was not paired as a replace is an exit from here on.
|
|
597
623
|
// eslint-disable-next-line @eslint-react/set-state-in-effect -- settles the close once the registry has answered
|
|
598
624
|
setIsCloseSettled(true);
|
|
625
|
+
// Read with the close, before the sheet can go inert.
|
|
626
|
+
setIsExitFocusHeld(hadFocusAtCloseRef.current);
|
|
599
627
|
}
|
|
600
628
|
}, [hasScrim, isOpen, shouldPresent]);
|
|
601
629
|
|
|
@@ -699,7 +727,44 @@ function StandaloneBottomSheet({
|
|
|
699
727
|
);
|
|
700
728
|
|
|
701
729
|
const {ref: modalityRef} = useInteractionModalityTracking();
|
|
702
|
-
|
|
730
|
+
// Ends the exit hold once focus has left the sheet: moved elsewhere, or
|
|
731
|
+
// dropped to the page because the focused control was removed or disabled.
|
|
732
|
+
// Native listeners, because React drops the `focusout` a browser sends while
|
|
733
|
+
// React removes the focused element, and Firefox and WebKit send no
|
|
734
|
+
// `focusout` for a removed element at all, so a subtree change is read too.
|
|
735
|
+
// Each reads the focus after the event. They attach only while a closing
|
|
736
|
+
// sheet holds focus, so a closed or open sheet observes nothing.
|
|
737
|
+
const isWatchingExitFocus =
|
|
738
|
+
!isOpen && isPresented && !isReplaced && isExitFocusHeld;
|
|
739
|
+
const exitFocusRef = useCallback(
|
|
740
|
+
(dialog: HTMLDialogElement | null) => {
|
|
741
|
+
if (dialog == null || !isWatchingExitFocus) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const releaseIfFocusLeft = () => {
|
|
745
|
+
if (!dialog.contains(dialog.ownerDocument.activeElement)) {
|
|
746
|
+
setIsExitFocusHeld(false);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
const onFocusOut = () => queueMicrotask(releaseIfFocusLeft);
|
|
750
|
+
dialog.addEventListener('focusout', onFocusOut);
|
|
751
|
+
const unobserve = observeMutations(
|
|
752
|
+
dialog,
|
|
753
|
+
{
|
|
754
|
+
childList: true,
|
|
755
|
+
subtree: true,
|
|
756
|
+
attributeFilter: ['disabled', 'hidden', 'inert'],
|
|
757
|
+
},
|
|
758
|
+
releaseIfFocusLeft,
|
|
759
|
+
);
|
|
760
|
+
return () => {
|
|
761
|
+
dialog.removeEventListener('focusout', onFocusOut);
|
|
762
|
+
unobserve();
|
|
763
|
+
};
|
|
764
|
+
},
|
|
765
|
+
[isWatchingExitFocus],
|
|
766
|
+
);
|
|
767
|
+
const dialogComposedRef = useMergedRefs(dialogRef, modalityRef, exitFocusRef);
|
|
703
768
|
useScrollLock(shouldPresent && hasScrim);
|
|
704
769
|
useDevWarning(
|
|
705
770
|
'BottomSheet',
|
|
@@ -769,7 +834,16 @@ function StandaloneBottomSheet({
|
|
|
769
834
|
// that contains no presented sheet is marked inert and hidden as well, so
|
|
770
835
|
// the state is explicit; one that does contain the top sheet is left alone,
|
|
771
836
|
// because inert on it would reach the top sheet too.
|
|
772
|
-
|
|
837
|
+
// A sheet that closes with focus inside it is neither inert nor hidden
|
|
838
|
+
// while that focus stays: `inert` would drop the focus to the page, and
|
|
839
|
+
// `aria-hidden` would hide the focused control. When the exit ends, the
|
|
840
|
+
// dialog closes and focus returns in the same task. When focus leaves the
|
|
841
|
+
// sheet first (into a sheet opened above it, for example), the sheet goes
|
|
842
|
+
// inert at once. Until the close settles, no closing sheet is inert, so the
|
|
843
|
+
// first commit of a close never hides focus.
|
|
844
|
+
const isExitHoldingFocus =
|
|
845
|
+
isExiting && !isReplaced && (!isCloseSettled || isExitFocusHeld);
|
|
846
|
+
const isInert = (isExiting && !isExitHoldingFocus) || stackView.isBlocked;
|
|
773
847
|
|
|
774
848
|
return (
|
|
775
849
|
<dialog
|
|
@@ -784,6 +858,7 @@ function StandaloneBottomSheet({
|
|
|
784
858
|
hasScrim && styles.scrim,
|
|
785
859
|
hasScrim && isEntrySettled && styles.scrimSettled,
|
|
786
860
|
hasScrim && isExiting && styles.scrimClosing,
|
|
861
|
+
isExitHoldingFocus && styles.exitHoldingFocus,
|
|
787
862
|
!hasScrim && styles.dialogNonModal,
|
|
788
863
|
)}
|
|
789
864
|
ref={dialogComposedRef}
|
|
@@ -805,6 +880,11 @@ function StandaloneBottomSheet({
|
|
|
805
880
|
hasScrim && stackView.depth > 0 ? stackView.depth : undefined
|
|
806
881
|
}
|
|
807
882
|
onCancel={handleCancel}
|
|
883
|
+
// A closing sheet that keeps focus takes no activation during its
|
|
884
|
+
// exit: no click and no key but Tab reaches the control that has focus.
|
|
885
|
+
onClickCapture={isExitHoldingFocus ? ignoreActivation : undefined}
|
|
886
|
+
onKeyDownCapture={isExitHoldingFocus ? ignoreKeyDuringExit : undefined}
|
|
887
|
+
onKeyUpCapture={isExitHoldingFocus ? ignoreKeyDuringExit : undefined}
|
|
808
888
|
// Presses inside the sheet do not reach handlers around where it is
|
|
809
889
|
// written. The dialog, not the panel, is the root, as in a switcher.
|
|
810
890
|
{...layerClickBoundary({onClick: handleClick})}>
|
|
@@ -866,12 +866,16 @@ function openModalsOutside(element: Element): HTMLDialogElement[] {
|
|
|
866
866
|
if (dialog === element || dialog.contains(element)) {
|
|
867
867
|
continue;
|
|
868
868
|
}
|
|
869
|
-
// A modal on its way out
|
|
870
|
-
//
|
|
871
|
-
//
|
|
869
|
+
// A modal on its way out blocks nothing any more: it is gone for this
|
|
870
|
+
// decision. A closing sheet keeps its dialog modal through the exit
|
|
871
|
+
// animation; it is inert and hidden then, unless focus was inside it at
|
|
872
|
+
// the close, so the registry's own record of the close counts too.
|
|
872
873
|
if (
|
|
873
874
|
dialog.hasAttribute('inert') ||
|
|
874
|
-
dialog.getAttribute('aria-hidden') === 'true'
|
|
875
|
+
dialog.getAttribute('aria-hidden') === 'true' ||
|
|
876
|
+
state.entries.some(
|
|
877
|
+
entry => !entry.isOpen && entry.getElement() === dialog,
|
|
878
|
+
)
|
|
875
879
|
) {
|
|
876
880
|
continue;
|
|
877
881
|
}
|