@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.
130 lines (129 loc) • 4.69 kB
JavaScript
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { safePolygon, useClientPoint, useDelayGroup, useDismiss, useFloatingRootContext, useFocus, 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 { OPEN_DELAY } from '../utils/constants.js';
import { translateOpenChangeReason } from '../../utils/translateOpenChangeReason.js';
import { useAfterExitAnimation } from '../../utils/useAfterExitAnimation.js';
export function useTooltipRoot(params) {
const {
open: externalOpen,
onOpenChange: onOpenChangeProp = () => {},
defaultOpen = false,
hoverable = true,
trackCursorAxis = 'none',
delay,
closeDelay
} = params;
const delayWithDefault = delay ?? OPEN_DELAY;
const closeDelayWithDefault = closeDelay ?? 0;
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: 'Tooltip',
state: 'open'
});
const onOpenChange = useEventCallback(onOpenChangeProp);
const setOpen = React.useCallback((nextOpen, event, reason) => {
onOpenChange(nextOpen, event, reason);
setOpenUnwrapped(nextOpen);
}, [onOpenChange, setOpenUnwrapped]);
const {
mounted,
setMounted,
transitionStatus
} = useTransitionStatus(open);
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 {
delay: groupDelay,
isInstantPhase,
currentId
} = useDelayGroup(context);
const openGroupDelay = typeof groupDelay === 'object' ? groupDelay.open : groupDelay;
const closeGroupDelay = typeof groupDelay === 'object' ? groupDelay.close : groupDelay;
let instantType = isInstantPhase ? 'delay' : instantTypeState;
if (!open && context.floatingId === currentId) {
instantType = instantTypeState;
}
const computedRestMs = openGroupDelay || delayWithDefault;
let computedCloseDelay = closeDelayWithDefault;
// A provider is present and the close delay is not set.
if (closeDelay == null && groupDelay !== 0) {
computedCloseDelay = closeGroupDelay;
}
const hover = useHover(context, {
mouseOnly: true,
move: false,
handleClose: hoverable && trackCursorAxis !== 'both' ? safePolygon() : null,
restMs: computedRestMs,
delay: {
close: computedCloseDelay
}
});
const focus = useFocus(context);
const dismiss = useDismiss(context, {
referencePress: true
});
const clientPoint = useClientPoint(context, {
enabled: trackCursorAxis !== 'none',
axis: trackCursorAxis === 'none' ? undefined : trackCursorAxis
});
const {
getReferenceProps: getRootTriggerProps,
getFloatingProps: getRootPopupProps
} = useInteractions([hover, focus, dismiss, clientPoint]);
return React.useMemo(() => ({
open,
setOpen,
mounted,
setMounted,
setTriggerElement,
positionerElement,
setPositionerElement,
popupRef,
getRootTriggerProps,
getRootPopupProps,
floatingRootContext: context,
instantType,
transitionStatus
}), [mounted, open, setMounted, positionerElement, setOpen, getRootTriggerProps, getRootPopupProps, context, instantType, transitionStatus]);
}