@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
JavaScript
;
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
};
}