@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.
122 lines (121 loc) • 4.13 kB
JavaScript
import * as React from 'react';
import { contains } from '@floating-ui/react/utils';
import { useButton } from '../../use-button/useButton.js';
import { mergeReactProps } from '../../utils/mergeReactProps.js';
import { useForkRef } from '../../utils/useForkRef.js';
import { useSelectRootContext } from '../root/SelectRootContext.js';
import { ownerDocument } from '../../utils/owner.js';
import { useFieldRootContext } from '../../field/root/FieldRootContext.js';
export function useSelectTrigger(parameters) {
const {
disabled = false,
rootRef: externalRef
} = parameters;
const {
open,
setOpen,
setTriggerElement,
selectionRef,
value,
fieldControlValidation,
setTouchModality,
positionerElement,
alignItemToTrigger,
readOnly
} = useSelectRootContext();
const {
labelId,
setTouched
} = useFieldRootContext();
const triggerRef = React.useRef(null);
const timeoutRef = React.useRef(-1);
const mergedRef = useForkRef(externalRef, triggerRef);
const {
getButtonProps,
buttonRef
} = useButton({
disabled,
buttonRef: mergedRef
});
const handleRef = useForkRef(buttonRef, setTriggerElement);
React.useEffect(() => {
if (open) {
// mousedown -> mouseup on selected item should not select within 400ms.
const timeoutId1 = window.setTimeout(() => {
selectionRef.current.allowSelectedMouseUp = true;
}, 400);
// mousedown -> move to unselected item -> mouseup should not select within 200ms.
const timeoutId2 = window.setTimeout(() => {
selectionRef.current.allowUnselectedMouseUp = true;
}, 200);
return () => {
clearTimeout(timeoutId1);
clearTimeout(timeoutId2);
};
}
selectionRef.current = {
allowSelectedMouseUp: false,
allowUnselectedMouseUp: false,
allowSelect: true
};
clearTimeout(timeoutRef.current);
return undefined;
}, [open, selectionRef]);
const getTriggerProps = React.useCallback(externalProps => {
return mergeReactProps(fieldControlValidation.getValidationProps(externalProps), {
'aria-labelledby': labelId,
'aria-readonly': readOnly || undefined,
tabIndex: 0,
// this is needed to make the button focused after click in Safari
ref: handleRef,
onFocus() {
// The popup element shouldn't obscure the focused trigger.
if (open && alignItemToTrigger) {
setOpen(false);
}
},
onBlur() {
setTouched(true);
fieldControlValidation.commitValidation(value);
},
onPointerMove({
pointerType
}) {
setTouchModality(pointerType === 'touch');
},
onPointerDown({
pointerType
}) {
setTouchModality(pointerType === 'touch');
},
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(positionerElement, mouseUpTarget) || contains(triggerRef.current, mouseUpTarget)) {
return;
}
setOpen(false, mouseEvent);
}
// Firefox can fire this upon mousedown
timeoutRef.current = window.setTimeout(() => {
doc.addEventListener('mouseup', handleMouseUp, {
once: true
});
});
}
}, getButtonProps());
}, [fieldControlValidation, labelId, readOnly, handleRef, getButtonProps, open, alignItemToTrigger, setOpen, setTouched, value, setTouchModality, positionerElement]);
return React.useMemo(() => ({
getTriggerProps,
rootRef: handleRef
}), [getTriggerProps, handleRef]);
}