@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.
111 lines (106 loc) • 4.82 kB
JavaScript
'use client';
import * as React from 'react';
import { useAnimationFrame } from '@base-ui-components/utils/useAnimationFrame';
import { useTimeout } from '@base-ui-components/utils/useTimeout';
import { EMPTY_OBJECT } from "../../utils/constants.js";
import { isMouseLikePointerType, isTypeableElement } from "../utils.js";
import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js";
import { REASONS } from "../../utils/reasons.js";
/**
* Opens or closes the floating element when clicking the reference element.
* @see https://floating-ui.com/docs/useClick
*/
export function useClick(context, props = {}) {
const store = 'rootStore' in context ? context.rootStore : context;
const dataRef = store.context.dataRef;
const {
enabled = true,
event: eventOption = 'click',
toggle = true,
ignoreMouse = false,
stickIfOpen = true,
touchOpenDelay = 0
} = props;
const pointerTypeRef = React.useRef(undefined);
const frame = useAnimationFrame();
const touchOpenTimeout = useTimeout();
const reference = React.useMemo(() => ({
onPointerDown(event) {
pointerTypeRef.current = event.pointerType;
},
onMouseDown(event) {
const pointerType = pointerTypeRef.current;
const nativeEvent = event.nativeEvent;
const open = store.select('open');
// Ignore all buttons except for the "main" button.
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
if (event.button !== 0 || eventOption === 'click' || isMouseLikePointerType(pointerType, true) && ignoreMouse) {
return;
}
const openEvent = dataRef.current.openEvent;
const openEventType = openEvent?.type;
const hasClickedOnInactiveTrigger = store.select('domReferenceElement') !== event.currentTarget;
const nextOpen = open && hasClickedOnInactiveTrigger || !(open && toggle && (openEvent && stickIfOpen ? openEventType === 'click' || openEventType === 'mousedown' : true));
// Animations sometimes won't run on a typeable element if using a rAF.
// Focus is always set on these elements. For touch, we may delay opening.
if (isTypeableElement(nativeEvent.target)) {
const details = createChangeEventDetails(REASONS.triggerPress, nativeEvent, nativeEvent.target);
if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
touchOpenTimeout.start(touchOpenDelay, () => {
store.setOpen(true, details);
});
} else {
store.setOpen(nextOpen, details);
}
return;
}
// Capture the currentTarget before the rAF.
// as React sets it to null after the event handler completes.
const eventCurrentTarget = event.currentTarget;
// Wait until focus is set on the element. This is an alternative to
// `event.preventDefault()` to avoid :focus-visible from appearing when using a pointer.
frame.request(() => {
const details = createChangeEventDetails(REASONS.triggerPress, nativeEvent, eventCurrentTarget);
if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
touchOpenTimeout.start(touchOpenDelay, () => {
store.setOpen(true, details);
});
} else {
store.setOpen(nextOpen, details);
}
});
},
onClick(event) {
if (eventOption === 'mousedown-only') {
return;
}
const pointerType = pointerTypeRef.current;
if (eventOption === 'mousedown' && pointerType) {
pointerTypeRef.current = undefined;
return;
}
if (isMouseLikePointerType(pointerType, true) && ignoreMouse) {
return;
}
const open = store.select('open');
const openEvent = dataRef.current.openEvent;
const openEventType = openEvent?.type;
const hasClickedOnInactiveTrigger = store.select('domReferenceElement') !== event.currentTarget;
const nextOpen = open && hasClickedOnInactiveTrigger || !(open && toggle && (openEvent && stickIfOpen ? openEventType === 'click' || openEventType === 'mousedown' || openEventType === 'keydown' || openEventType === 'keyup' : true));
const details = createChangeEventDetails(REASONS.triggerPress, event.nativeEvent, event.currentTarget);
if (nextOpen && pointerType === 'touch' && touchOpenDelay > 0) {
touchOpenTimeout.start(touchOpenDelay, () => {
store.setOpen(true, details);
});
} else {
store.setOpen(nextOpen, details);
}
},
onKeyDown() {
pointerTypeRef.current = undefined;
}
}), [dataRef, eventOption, ignoreMouse, store, stickIfOpen, toggle, frame, touchOpenTimeout, touchOpenDelay]);
return React.useMemo(() => enabled ? {
reference
} : EMPTY_OBJECT, [enabled, reference]);
}