UNPKG

@base-ui-components/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.

158 lines (155 loc) 5.92 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useImplicitActiveTrigger = useImplicitActiveTrigger; exports.useOpenStateTransitions = useOpenStateTransitions; exports.useTriggerDataForwarding = useTriggerDataForwarding; exports.useTriggerRegistration = useTriggerRegistration; var React = _interopRequireWildcard(require("react")); var _useStableCallback = require("@base-ui-components/utils/useStableCallback"); var _useIsoLayoutEffect = require("@base-ui-components/utils/useIsoLayoutEffect"); var _useTransitionStatus = require("../useTransitionStatus"); var _useOpenChangeComplete = require("../useOpenChangeComplete"); /** * Returns a callback ref that registers/unregisters the trigger element in the store. * * @param store The Store instance where the trigger should be registered. */ function useTriggerRegistration(id, store) { // Keep track of the currently registered element to unregister it on unmount or id change. const registeredElementId = React.useRef(null); return React.useCallback(element => { if (id === undefined) { return undefined; } if (registeredElementId.current !== null) { store.context.triggerElements.delete(registeredElementId.current); registeredElementId.current = null; } if (element !== null) { registeredElementId.current = id; store.context.triggerElements.add(id, element); return () => { if (registeredElementId.current !== null) { store.context.triggerElements.delete(registeredElementId.current); registeredElementId.current = null; } }; } return undefined; }, [store, id]); } /** * Sets up trigger data forwarding to the store. * * @param triggerId Id of the trigger. * @param triggerElement The trigger DOM element. * @param store The Store instance managing the popup state. * @param stateUpdates An object with state updates to apply when the trigger is active. */ function useTriggerDataForwarding(triggerId, triggerElement, store, stateUpdates) { const isMountedByThisTrigger = store.useState('isMountedByTrigger', triggerId); const baseRegisterTrigger = useTriggerRegistration(triggerId, store); const registerTrigger = (0, _useStableCallback.useStableCallback)(element => { const cleanup = baseRegisterTrigger(element); if (element !== null && store.select('open') && store.select('activeTriggerId') == null) { // This runs when popup is open, but no active trigger is set. // It can happen when using controlled mode and the trigger is mounted after opening or if `triggerId` prop is not set explicitly. // In such cases the first trigger to run this code becomes the active trigger (store.select('activeTriggerId') should not return null after that). // This is mostly for compatibility with contained triggers where no explicit `triggerId` was required in controlled mode. store.update({ activeTriggerId: triggerId, activeTriggerElement: element, ...stateUpdates }); } return cleanup; }); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (isMountedByThisTrigger) { store.update({ activeTriggerElement: triggerElement, ...stateUpdates }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMountedByThisTrigger, store, triggerElement, ...Object.values(stateUpdates)]); return { registerTrigger, isMountedByThisTrigger }; } /** * Ensures that when there's only one trigger element registered, it is set as the active trigger. * This allows controlled popups to work correctly without an explicit triggerId, maintaining compatibility * with the contained triggers. * * This should be called on the Root part. * * @param open Whether the popup is open. * @param store The Store instance managing the popup state. */ function useImplicitActiveTrigger(store) { const open = store.useState('open'); (0, _useIsoLayoutEffect.useIsoLayoutEffect)(() => { if (open && !store.select('activeTriggerId') && store.context.triggerElements.size === 1) { const iteratorResult = store.context.triggerElements.entries().next(); if (!iteratorResult.done) { const [implicitTriggerId, implicitTriggerElement] = iteratorResult.value; store.update({ activeTriggerId: implicitTriggerId, activeTriggerElement: implicitTriggerElement }); } } }, [open, store]); } /** * Mangages the mounted state of the popup. * Sets up the transition status listeners and handles unmounting when needed. * Updates the `mounted` and `transitionStatus` states in the store. * * @param open Whether the popup is open. * @param store The Store instance managing the popup state. * @param onUnmount Optional callback to be called when the popup is unmounted. * * @returns A function to forcibly unmount the popup. */ function useOpenStateTransitions(open, store, onUnmount) { const { mounted, setMounted, transitionStatus } = (0, _useTransitionStatus.useTransitionStatus)(open); store.useSyncedValues({ mounted, transitionStatus }); const forceUnmount = (0, _useStableCallback.useStableCallback)(() => { setMounted(false); store.update({ activeTriggerId: null, activeTriggerElement: null, mounted: false }); onUnmount?.(); store.context.onOpenChangeComplete?.(false); }); const preventUnmountingOnClose = store.useState('preventUnmountingOnClose'); (0, _useOpenChangeComplete.useOpenChangeComplete)({ enabled: !preventUnmountingOnClose, open, ref: store.context.popupRef, onComplete() { if (!open) { forceUnmount(); } } }); return { forceUnmount, transitionStatus }; }