/** * Solid port of Base UI React's * `packages/react/src/utils/popups/useTriggerFocusGuards.ts`. * * Provides focus guard handlers for popup triggers (Popover, Menu). * * When the popup is open, invisible focus guard elements are placed * before and after the trigger. These handlers close the popup and move * focus to the appropriate tabbable element when the guards receive * focus (i.e. when the user tabs out). * * Port notes: * - `React.useRef` → plain `{ current: null }` mutable ref literal, the * same `MutableRef` shape the rest of the Solid port uses. * - `useStableCallback` → `createStableCallback`. In Solid the trampoline * keeps identity stable across re-renders (although Solid component * bodies don't re-run, the trampoline still insulates consumers that * capture the callback reference into a long-lived structure). * - `ReactDOM.flushSync` has no direct Solid equivalent — Solid batches * updates synchronously inside event handlers already, so the * `flushSync` wrapper is dropped. * - `React.FocusEvent` → native `FocusEvent`. `event.nativeEvent` is * already `FocusEvent` here, and `event.currentTarget` is the guard * element. * - `TriggerFocusGuardStore` is kept as a minimal structural interface * so it can match both the in-flight `PopoverStore` and `MenuStore` * ports without introducing a hard dependency on the real `PopupStore` * (which is being ported on a parallel track). */ import { type BaseUIChangeEventDetails, createChangeEventDetails, REASONS, } from "../change-event-details"; import { contains } from "../dom/contains"; import type { MutableRef } from "../floating-ui/types"; import { type FocusableElement, getNextTabbable, getTabbableAfterElement, getTabbableBeforeElement, isOutsideEvent, } from "../floating-ui/utils/tabbable"; import { createStableCallback } from "../stable-callback"; /** * Minimal store interface required by the focus guard hook. * Both PopoverStore and MenuStore satisfy this interface. * * Kept as a structural shape rather than re-importing the real popup * store, so this file remains independent of the parallel `PopupStore` * port track. */ export interface TriggerFocusGuardStore { setOpen( open: boolean, eventDetails: BaseUIChangeEventDetails, ): void; select(key: "positionerElement"): HTMLElement | null; context: { readonly beforeContentFocusGuardRef: MutableRef; readonly triggerFocusTargetRef: MutableRef; }; } /** * Provides focus guard handlers for popup triggers (Popover, Menu). * * When the popup is open, invisible focus guard elements are placed * before and after the trigger. These handlers close the popup and move * focus to the appropriate tabbable element when the guards receive * focus (i.e. when the user tabs out). */ export function useTriggerFocusGuards( store: TriggerFocusGuardStore, triggerElementRef: MutableRef, ): { preFocusGuardRef: MutableRef; handlePreFocusGuardFocus: (event: FocusEvent) => void; handleFocusTargetFocus: (event: FocusEvent) => void; } { const preFocusGuardRef: MutableRef = { current: null }; const handlePreFocusGuardFocus = createStableCallback< (event: FocusEvent) => void >(() => (event: FocusEvent) => { store.setOpen( false, createChangeEventDetails( REASONS.focusOut, event, event.currentTarget as HTMLElement, ), ); const previousTabbable: FocusableElement | null = getTabbableBeforeElement( preFocusGuardRef.current, ); previousTabbable?.focus(); }); const handleFocusTargetFocus = createStableCallback< (event: FocusEvent) => void >(() => (event: FocusEvent) => { const positionerElement = store.select("positionerElement"); if (positionerElement && isOutsideEvent(event, positionerElement)) { store.context.beforeContentFocusGuardRef.current?.focus(); } else { store.setOpen( false, createChangeEventDetails( REASONS.focusOut, event, event.currentTarget as HTMLElement, ), ); let nextTabbable = getTabbableAfterElement( store.context.triggerFocusTargetRef.current || triggerElementRef.current, ); while ( nextTabbable !== null && contains(positionerElement, nextTabbable) ) { const prevTabbable = nextTabbable; nextTabbable = getNextTabbable(nextTabbable); if (nextTabbable === prevTabbable) { break; } } nextTabbable?.focus(); } }); return { preFocusGuardRef, handlePreFocusGuardFocus, handleFocusTargetFocus }; }