@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.
184 lines (182 loc) • 7.07 kB
JavaScript
import * as React from 'react';
import { isElement } from '@floating-ui/utils/dom';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { getDocument, getTarget, isMouseLikePointerType } from "../utils.js";
import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js";
import { REASONS } from "../../utils/reasons.js";
import { useFloatingParentNodeId, useFloatingTree } from "../components/FloatingTree.js";
import { isInteractiveElement, safePolygonIdentifier, useHoverInteractionSharedState } from "./useHoverInteractionSharedState.js";
const clickLikeEvents = new Set(['click', 'mousedown']);
/**
* Provides hover interactions that should be attached to the floating element.
*/
export function useHoverFloatingInteraction(context, parameters = {}) {
const store = 'rootStore' in context ? context.rootStore : context;
const open = store.useState('open');
const floatingElement = store.useState('floatingElement');
const domReferenceElement = store.useState('domReferenceElement');
const {
dataRef
} = store.context;
const {
enabled = true,
closeDelay: closeDelayProp = 0,
externalTree
} = parameters;
const {
pointerTypeRef,
interactedInsideRef,
handlerRef,
performedPointerEventsMutationRef,
unbindMouseMoveRef,
restTimeoutPendingRef,
openChangeTimeout: openChangeTimeout,
handleCloseOptionsRef
} = useHoverInteractionSharedState(store);
const tree = useFloatingTree(externalTree);
const parentId = useFloatingParentNodeId();
const isClickLikeOpenEvent = useStableCallback(() => {
if (interactedInsideRef.current) {
return true;
}
return dataRef.current.openEvent ? clickLikeEvents.has(dataRef.current.openEvent.type) : false;
});
const isHoverOpen = useStableCallback(() => {
const type = dataRef.current.openEvent?.type;
return type?.includes('mouse') && type !== 'mousedown';
});
const closeWithDelay = React.useCallback((event, runElseBranch = true) => {
const closeDelay = getDelay(closeDelayProp, pointerTypeRef.current);
if (closeDelay && !handlerRef.current) {
openChangeTimeout.start(closeDelay, () => store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event)));
} else if (runElseBranch) {
openChangeTimeout.clear();
store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
}
}, [closeDelayProp, handlerRef, store, pointerTypeRef, openChangeTimeout]);
const cleanupMouseMoveHandler = useStableCallback(() => {
unbindMouseMoveRef.current();
handlerRef.current = undefined;
});
const clearPointerEvents = useStableCallback(() => {
if (performedPointerEventsMutationRef.current) {
const body = getDocument(floatingElement).body;
body.style.pointerEvents = '';
body.removeAttribute(safePolygonIdentifier);
performedPointerEventsMutationRef.current = false;
}
});
const handleInteractInside = useStableCallback(event => {
const target = getTarget(event);
if (!isInteractiveElement(target)) {
interactedInsideRef.current = false;
return;
}
interactedInsideRef.current = true;
});
useIsoLayoutEffect(() => {
if (!open) {
pointerTypeRef.current = undefined;
restTimeoutPendingRef.current = false;
interactedInsideRef.current = false;
cleanupMouseMoveHandler();
clearPointerEvents();
}
}, [open, pointerTypeRef, restTimeoutPendingRef, interactedInsideRef, cleanupMouseMoveHandler, clearPointerEvents]);
React.useEffect(() => {
return () => {
cleanupMouseMoveHandler();
};
}, [cleanupMouseMoveHandler]);
React.useEffect(() => {
return clearPointerEvents;
}, [clearPointerEvents]);
useIsoLayoutEffect(() => {
if (!enabled) {
return undefined;
}
if (open && handleCloseOptionsRef.current?.blockPointerEvents && isHoverOpen() && isElement(domReferenceElement) && floatingElement) {
performedPointerEventsMutationRef.current = true;
const body = getDocument(floatingElement).body;
body.setAttribute(safePolygonIdentifier, '');
const ref = domReferenceElement;
const floatingEl = floatingElement;
const parentFloating = tree?.nodesRef.current.find(node => node.id === parentId)?.context?.elements.floating;
if (parentFloating) {
parentFloating.style.pointerEvents = '';
}
body.style.pointerEvents = 'none';
ref.style.pointerEvents = 'auto';
floatingEl.style.pointerEvents = 'auto';
return () => {
body.style.pointerEvents = '';
ref.style.pointerEvents = '';
floatingEl.style.pointerEvents = '';
};
}
return undefined;
}, [enabled, open, domReferenceElement, floatingElement, handleCloseOptionsRef, isHoverOpen, tree, parentId, performedPointerEventsMutationRef]);
React.useEffect(() => {
if (!enabled) {
return undefined;
}
// Ensure the floating element closes after scrolling even if the pointer
// did not move.
// https://github.com/floating-ui/floating-ui/discussions/1692
function onScrollMouseLeave(event) {
if (isClickLikeOpenEvent()) {
return;
}
if (!dataRef.current.floatingContext) {
return;
}
const triggerElements = store.context.triggerElements;
if (event.relatedTarget && triggerElements.hasElement(event.relatedTarget)) {
// If the mouse is leaving the reference element to another trigger, don't explicitly close the popup
// as it will be moved.
return;
}
clearPointerEvents();
cleanupMouseMoveHandler();
if (!isClickLikeOpenEvent()) {
closeWithDelay(event);
}
}
function onFloatingMouseEnter(event) {
openChangeTimeout.clear();
clearPointerEvents();
handlerRef.current?.(event);
cleanupMouseMoveHandler();
}
function onFloatingMouseLeave(event) {
if (!isClickLikeOpenEvent()) {
closeWithDelay(event, false);
}
}
const floating = floatingElement;
if (floating) {
floating.addEventListener('mouseleave', onScrollMouseLeave);
floating.addEventListener('mouseenter', onFloatingMouseEnter);
floating.addEventListener('mouseleave', onFloatingMouseLeave);
floating.addEventListener('pointerdown', handleInteractInside, true);
}
return () => {
if (floating) {
floating.removeEventListener('mouseleave', onScrollMouseLeave);
floating.removeEventListener('mouseenter', onFloatingMouseEnter);
floating.removeEventListener('mouseleave', onFloatingMouseLeave);
floating.removeEventListener('pointerdown', handleInteractInside, true);
}
};
});
}
export function getDelay(value, pointerType) {
if (pointerType && !isMouseLikePointerType(pointerType)) {
return 0;
}
if (typeof value === 'function') {
return value();
}
return value;
}