@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.
93 lines (91 loc) • 3.35 kB
JavaScript
'use client';
import * as React from 'react';
import { contains } from '@floating-ui/react/utils';
import { useButton } from '../../use-button/useButton.js';
import { useForkRef } from '../../utils/useForkRef.js';
import { mergeReactProps } from '../../utils/mergeReactProps.js';
import { ownerDocument } from '../../utils/owner.js';
export function useMenuTrigger(parameters) {
const {
disabled = false,
rootRef: externalRef,
open,
setOpen,
setTriggerElement,
positionerRef,
allowMouseUpTriggerRef
} = parameters;
const triggerRef = React.useRef(null);
const mergedRef = useForkRef(externalRef, triggerRef);
const eventHandlerTimeoutRef = React.useRef(-1);
const allowMouseUpTriggerTimeoutRef = React.useRef(-1);
const {
getButtonProps,
buttonRef
} = useButton({
disabled,
buttonRef: mergedRef
});
const handleRef = useForkRef(buttonRef, setTriggerElement);
React.useEffect(() => {
if (open) {
// mousedown -> mouseup on menu item should not trigger it within 200ms.
allowMouseUpTriggerTimeoutRef.current = window.setTimeout(() => {
allowMouseUpTriggerRef.current = true;
}, 200);
return () => {
clearTimeout(allowMouseUpTriggerTimeoutRef.current);
allowMouseUpTriggerTimeoutRef.current = -1;
};
}
allowMouseUpTriggerRef.current = false;
if (eventHandlerTimeoutRef.current !== -1) {
clearTimeout(eventHandlerTimeoutRef.current);
eventHandlerTimeoutRef.current = -1;
}
return undefined;
}, [allowMouseUpTriggerRef, open]);
const getTriggerProps = React.useCallback(externalProps => {
return mergeReactProps(externalProps, {
'aria-haspopup': 'menu',
tabIndex: 0,
// this is needed to make the button focused after click in Safari
ref: handleRef,
onMouseDown: event => {
if (open) {
return;
}
const doc = ownerDocument(event.currentTarget);
function handleMouseUp(mouseEvent) {
if (!triggerRef.current) {
return;
}
const mouseUpTarget = mouseEvent.target;
const triggerRect = triggerRef.current.getBoundingClientRect();
const isInsideTrigger = mouseEvent.clientX >= triggerRect.left && mouseEvent.clientX <= triggerRect.right && mouseEvent.clientY >= triggerRect.top && mouseEvent.clientY <= triggerRect.bottom;
if (isInsideTrigger || contains(positionerRef.current, mouseUpTarget) || contains(triggerRef.current, mouseUpTarget)) {
return;
}
setOpen(false, mouseEvent);
}
// Firefox can fire this upon mousedown
eventHandlerTimeoutRef.current = window.setTimeout(() => {
doc.addEventListener('mouseup', handleMouseUp, {
once: true
});
});
},
onClick: () => {
allowMouseUpTriggerRef.current = false;
if (allowMouseUpTriggerTimeoutRef.current !== -1) {
clearTimeout(allowMouseUpTriggerTimeoutRef.current);
allowMouseUpTriggerTimeoutRef.current = -1;
}
}
}, getButtonProps());
}, [getButtonProps, handleRef, open, setOpen, positionerRef, allowMouseUpTriggerRef]);
return React.useMemo(() => ({
getTriggerProps,
triggerRef: handleRef
}), [getTriggerProps, handleRef]);
}