From 65be45bcb50aeeb9c8e6082dc8e583f8c1794593 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 13:53:48 -0700 Subject: [PATCH] refactor(Selector): the presentation controller holds its choice through the layer runtime's presentation hold useSelectorPresentation (Selector and MultiSelector) kept two latches of its own: activePresentationRef, the presentation an opening chose, and isSheetPresented, the sheet kept mounted after it closes until BottomSheet's focus handoff reaches the trigger. Both now use the layer runtime's presentation hold (Layer/presentationHold): createPresentationHold taken at show, and usePresentationHold over the sheet's open state, released where the old state was cleared. No behavior change; Selector and MultiSelector tests pass unchanged. --- .changeset/selector-presentation-hold.md | 7 +++ .../src/Selector/useSelectorPresentation.ts | 47 +++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 .changeset/selector-presentation-hold.md diff --git a/.changeset/selector-presentation-hold.md b/.changeset/selector-presentation-hold.md new file mode 100644 index 0000000000000..85450f1a4805d --- /dev/null +++ b/.changeset/selector-presentation-hold.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/core': patch +--- + +[chore] Internal: Selector and MultiSelector keep the presentation an opening chose, and a closing sheet mounted until its focus handoff, through the layer runtime's presentation hold (Layer/presentationHold.ts). No behavior change. + +@cixzhang diff --git a/packages/core/src/Selector/useSelectorPresentation.ts b/packages/core/src/Selector/useSelectorPresentation.ts index ab9be1ba04dab..18208685839d8 100644 --- a/packages/core/src/Selector/useSelectorPresentation.ts +++ b/packages/core/src/Selector/useSelectorPresentation.ts @@ -27,6 +27,10 @@ import { type ResolvedAdaptivePresentation, } from '../hooks/useAdaptivePresentation'; import {useFocusReturnVisibility} from '../hooks/useFocusReturnVisibility'; +import { + createPresentationHold, + usePresentationHold, +} from '../Layer/presentationHold'; interface UseSelectorPresentationOptions { presentation: AdaptivePresentation; @@ -57,14 +61,20 @@ export function useSelectorPresentation({ triggerRef, }: UseSelectorPresentationOptions): SelectorPresentationController { const resolvedPresentation = useAdaptivePresentation(presentation); - const activePresentationRef = - useRef(resolvedPresentation); + // The layer runtime's presentation holds (Layer/presentationHold): the + // presentation an opening chose, taken as it shows; and the sheet, held from + // the render that opens it until its close settles. BottomSheet owns its + // exit animation and final-focus handoff, so the sheet stays mounted after + // the controlled open state turns false until that handoff reaches the + // trigger; then consumers may remove the hidden surface tree. + const [activePresentationHold] = useState(() => + createPresentationHold(), + ); const [isSheetOpen, setIsSheetOpen] = useState(false); const isSheetOpenRef = useRef(false); - // BottomSheet owns its exit animation and final-focus handoff. Keep it mounted - // after the controlled open state turns false until that handoff reaches the - // trigger, then consumers may remove the hidden surface tree. - const [isSheetPresented, setIsSheetPresented] = useState(false); + const {isHeld: isSheetPresented, release: releaseSheet} = usePresentationHold( + {isOpen: isSheetOpen, presentation: true}, + ); const onHideRef = useRef(onHide); const onShowRef = useRef(onShow); const didShowPopoverRef = useRef(false); @@ -95,20 +105,25 @@ export function useSelectorPresentation({ const {hide: hidePopover, show: showPopover} = popover; const show = useCallback((): boolean => { - activePresentationRef.current = resolvedPresentation; + activePresentationHold.take(resolvedPresentation); if (resolvedPresentation === 'bottom-sheet') { resetFocusReturn(); onShowRef.current(); isSheetOpenRef.current = true; - setIsSheetPresented(true); setIsSheetOpen(true); return true; } - setIsSheetPresented(false); + releaseSheet(); didShowPopoverRef.current = false; showPopover(); return didShowPopoverRef.current; - }, [resetFocusReturn, resolvedPresentation, showPopover]); + }, [ + activePresentationHold, + releaseSheet, + resetFocusReturn, + resolvedPresentation, + showPopover, + ]); const hide = useCallback(() => { if (isSheetOpenRef.current) { @@ -126,15 +141,17 @@ export function useSelectorPresentation({ onFocusReturnTargetFocus(); if ( !isSheetOpenRef.current && - activePresentationRef.current === 'bottom-sheet' + activePresentationHold.current() === 'bottom-sheet' ) { // BottomSheet focuses finalFocusRef only after its exit completes. That // focus event is the lifecycle signal that the retained sheet can now - // leave the tree without dropping focus to the document body. - setIsSheetPresented(false); + // leave the tree without dropping focus to the document body: its + // close has settled. + activePresentationHold.release(); + releaseSheet(); } }, - [onFocusReturnTargetFocus], + [activePresentationHold, onFocusReturnTargetFocus, releaseSheet], ); const handleSheetOpenChange = useCallback( @@ -151,7 +168,7 @@ export function useSelectorPresentation({ const isOpen = popover.isOpen || isSheetOpen; const activePresentation = isOpen || isSheetPresented - ? activePresentationRef.current + ? (activePresentationHold.current() ?? resolvedPresentation) : resolvedPresentation; return {