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.

105 lines 3.79 kB
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { safePolygon, useDismiss, useFloatingRootContext, useHover, useInteractions } from '@floating-ui/react'; import { useControlled } from '../../utils/useControlled.js'; import { useTransitionStatus } from '../../utils/useTransitionStatus.js'; import { useEventCallback } from '../../utils/useEventCallback.js'; import { useFocusExtended } from '../utils/useFocusExtended.js'; import { OPEN_DELAY, CLOSE_DELAY } from '../utils/constants.js'; import { translateOpenChangeReason } from '../../utils/translateOpenChangeReason.js'; import { useAfterExitAnimation } from '../../utils/useAfterExitAnimation.js'; export function usePreviewCardRoot(params) { const { open: externalOpen, onOpenChange: onOpenChangeProp = () => {}, defaultOpen = false, delay, closeDelay } = params; const delayWithDefault = delay ?? OPEN_DELAY; const closeDelayWithDefault = closeDelay ?? CLOSE_DELAY; const [triggerElement, setTriggerElement] = React.useState(null); const [positionerElement, setPositionerElement] = React.useState(null); const [instantTypeState, setInstantTypeState] = React.useState(); const popupRef = React.useRef(null); const [open, setOpenUnwrapped] = useControlled({ controlled: externalOpen, default: defaultOpen, name: 'PreviewCard', state: 'open' }); const onOpenChange = useEventCallback(onOpenChangeProp); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); const setOpen = useEventCallback((nextOpen, event, reason) => { onOpenChange(nextOpen, event, reason); setOpenUnwrapped(nextOpen); }); useAfterExitAnimation({ open, animatedElementRef: popupRef, onFinished: () => setMounted(false) }); const context = useFloatingRootContext({ elements: { reference: triggerElement, floating: positionerElement }, open, onOpenChange(openValue, eventValue, reasonValue) { const isHover = reasonValue === 'hover' || reasonValue === 'safe-polygon'; const isFocusOpen = openValue && reasonValue === 'focus'; const isDismissClose = !openValue && (reasonValue === 'reference-press' || reasonValue === 'escape-key'); function changeState() { setOpen(openValue, eventValue, translateOpenChangeReason(reasonValue)); } if (isHover) { // If a hover reason is provided, we need to flush the state synchronously. This ensures // `node.getAnimations()` knows about the new state. ReactDOM.flushSync(changeState); } else { changeState(); } if (isFocusOpen || isDismissClose) { setInstantTypeState(isFocusOpen ? 'focus' : 'dismiss'); } else if (reasonValue === 'hover') { setInstantTypeState(undefined); } } }); const instantType = instantTypeState; const computedRestMs = delayWithDefault; const hover = useHover(context, { mouseOnly: true, move: false, handleClose: safePolygon(), restMs: computedRestMs, delay: { close: closeDelayWithDefault } }); const focus = useFocusExtended(context); const dismiss = useDismiss(context); const { getReferenceProps: getRootTriggerProps, getFloatingProps: getRootPopupProps } = useInteractions([hover, focus, dismiss]); return React.useMemo(() => ({ open, setOpen, mounted, setMounted, setTriggerElement, positionerElement, setPositionerElement, popupRef, getRootTriggerProps, getRootPopupProps, floatingRootContext: context, instantType, transitionStatus }), [mounted, open, setMounted, setOpen, positionerElement, getRootTriggerProps, getRootPopupProps, context, instantType, transitionStatus]); }