@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.
144 lines • 4.95 kB
JavaScript
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { safePolygon, useClick, useDismiss, useFloatingRootContext, useHover, useInteractions, useRole } from '@floating-ui/react';
import { useControlled } from '../../utils/useControlled.js';
import { useEventCallback } from '../../utils/useEventCallback.js';
import { useTransitionStatus } from '../../utils/useTransitionStatus.js';
import { PATIENT_CLICK_THRESHOLD, OPEN_DELAY } from '../utils/constants.js';
import { mergeReactProps } from '../../utils/mergeReactProps.js';
import { useOpenInteractionType } from '../../utils/useOpenInteractionType.js';
import { translateOpenChangeReason } from '../../utils/translateOpenChangeReason.js';
import { useAfterExitAnimation } from '../../utils/useAfterExitAnimation.js';
export function usePopoverRoot(params) {
const {
open: externalOpen,
onOpenChange: onOpenChangeProp = () => {},
defaultOpen = false,
delay,
closeDelay,
openOnHover = false
} = params;
const delayWithDefault = delay ?? OPEN_DELAY;
const closeDelayWithDefault = closeDelay ?? 0;
const [instantType, setInstantType] = React.useState();
const [titleId, setTitleId] = React.useState();
const [descriptionId, setDescriptionId] = React.useState();
const [triggerElement, setTriggerElement] = React.useState(null);
const [positionerElement, setPositionerElement] = React.useState(null);
const [openReason, setOpenReason] = React.useState(null);
const [clickEnabled, setClickEnabled] = React.useState(true);
const popupRef = React.useRef(null);
const clickEnabledTimeoutRef = React.useRef(-1);
const [open, setOpenUnwrapped] = useControlled({
controlled: externalOpen,
default: defaultOpen,
name: 'Popover',
state: 'open'
});
if (!open && !clickEnabled) {
setClickEnabled(true);
}
const onOpenChange = useEventCallback(onOpenChangeProp);
const {
mounted,
setMounted,
transitionStatus
} = useTransitionStatus(open);
const setOpen = useEventCallback((nextOpen, event, reason) => {
onOpenChange(nextOpen, event, reason);
setOpenUnwrapped(nextOpen);
if (nextOpen) {
setOpenReason(reason ?? null);
}
});
useAfterExitAnimation({
open,
animatedElementRef: popupRef,
onFinished: () => {
setMounted(false);
setOpenReason(null);
}
});
React.useEffect(() => {
return () => {
clearTimeout(clickEnabledTimeoutRef.current);
};
}, []);
const context = useFloatingRootContext({
elements: {
reference: triggerElement,
floating: positionerElement
},
open,
onOpenChange(openValue, eventValue, reasonValue) {
const isHover = reasonValue === 'hover' || reasonValue === 'safe-polygon';
const isKeyboardClick = reasonValue === 'click' && eventValue.detail === 0;
const isDismissClose = !openValue && (reasonValue === 'escape-key' || reasonValue == null);
function changeState() {
setOpen(openValue, eventValue, translateOpenChangeReason(reasonValue));
}
if (isHover) {
// Prevent impatient clicks from unexpectedly closing the popover.
setClickEnabled(false);
clearTimeout(clickEnabledTimeoutRef.current);
clickEnabledTimeoutRef.current = window.setTimeout(() => {
setClickEnabled(true);
}, PATIENT_CLICK_THRESHOLD);
ReactDOM.flushSync(changeState);
} else {
changeState();
}
if (isKeyboardClick || isDismissClose) {
setInstantType(isKeyboardClick ? 'click' : 'dismiss');
} else {
setInstantType(undefined);
}
}
});
const computedRestMs = delayWithDefault;
const hover = useHover(context, {
enabled: openOnHover,
mouseOnly: true,
move: false,
handleClose: safePolygon(),
restMs: computedRestMs,
delay: {
close: closeDelayWithDefault
}
});
const click = useClick(context, {
enabled: clickEnabled,
stickIfOpen: false
});
const dismiss = useDismiss(context);
const role = useRole(context);
const {
getReferenceProps,
getFloatingProps
} = useInteractions([hover, click, dismiss, role]);
const {
openMethod,
triggerProps
} = useOpenInteractionType(open);
return React.useMemo(() => ({
open,
setOpen,
mounted,
setMounted,
transitionStatus,
setTriggerElement,
positionerElement,
setPositionerElement,
popupRef,
titleId,
setTitleId,
descriptionId,
setDescriptionId,
getRootTriggerProps: externalProps => getReferenceProps(mergeReactProps(externalProps, triggerProps)),
getRootPopupProps: getFloatingProps,
floatingRootContext: context,
instantType,
openMethod,
openReason
}), [mounted, open, setMounted, setOpen, transitionStatus, positionerElement, titleId, descriptionId, getReferenceProps, getFloatingProps, context, instantType, openMethod, triggerProps, openReason]);
}