UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

55 lines (52 loc) 2.3 kB
'use client'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { useStableCallback } from '@base-ui/utils/useStableCallback'; import { contains, getNextTabbable, getTabbableAfterElement, getTabbableBeforeElement, isOutsideEvent } from "../../floating-ui-react/utils.js"; import { createChangeEventDetails } from "../../internals/createBaseUIEventDetails.js"; import { REASONS } from "../../internals/reasons.js"; /** * Minimal store interface required by the focus guard hook. * Both PopoverStore and MenuStore satisfy this interface. */ /** * 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, triggerElementRef) { const preFocusGuardRef = React.useRef(null); const handlePreFocusGuardFocus = useStableCallback(event => { ReactDOM.flushSync(() => { store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event.nativeEvent, event.currentTarget)); }); const previousTabbable = getTabbableBeforeElement(preFocusGuardRef.current); previousTabbable?.focus(); }); const handleFocusTargetFocus = useStableCallback(event => { const positionerElement = store.select('positionerElement'); if (positionerElement && isOutsideEvent(event, positionerElement)) { store.context.beforeContentFocusGuardRef.current?.focus(); } else { ReactDOM.flushSync(() => { store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event.nativeEvent, event.currentTarget)); }); 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 }; }