@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.
@@ -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;AAuDhD,YAAY,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AA6JhF,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;AA21BxD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA6BlD;yBA7Be,WAAW"}
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
- // Read before the browser's focus fixup for the exiting sheet's
412
- // `inert` runs (at the next rendering opportunity), so this is where
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
- const dialogComposedRef = useMergedRefs(dialogRef, modalityRef);
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
- const isInert = isExiting || stackView.isBlocked;
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 (a closing sheet keeps its dialog modal through
597
- // the exit animation, inert and hidden) blocks nothing any more: it is
598
- // gone for this decision.
599
- if (dialog.hasAttribute('inert') || dialog.getAttribute('aria-hidden') === 'true') {
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.4931743",
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.4931743",
869
- "@astryxdesign/cli": "0.7.7-canary.4931743",
870
- "@astryxdesign/interaction-harness": "0.7.7-canary.4931743",
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
- // Read before the browser's focus fixup for the exiting sheet's
585
- // `inert` runs (at the next rendering opportunity), so this is where
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
- const dialogComposedRef = useMergedRefs(dialogRef, modalityRef);
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
- const isInert = isExiting || stackView.isBlocked;
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 (a closing sheet keeps its dialog modal through
870
- // the exit animation, inert and hidden) blocks nothing any more: it is
871
- // gone for this decision.
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
  }