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.

144 lines 4.95 kB
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]); }