@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.
206 lines (199 loc) • 7.63 kB
JavaScript
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SelectTrigger = void 0;
var React = _interopRequireWildcard(require("react"));
var _owner = require("@base-ui-components/utils/owner");
var _useTimeout = require("@base-ui-components/utils/useTimeout");
var _useEventCallback = require("@base-ui-components/utils/useEventCallback");
var _useMergedRefs = require("@base-ui-components/utils/useMergedRefs");
var _useLatestRef = require("@base-ui-components/utils/useLatestRef");
var _store = require("@base-ui-components/utils/store");
var _SelectRootContext = require("../root/SelectRootContext");
var _FieldRootContext = require("../../field/root/FieldRootContext");
var _popupStateMapping = require("../../utils/popupStateMapping");
var _constants = require("../../field/utils/constants");
var _useRenderElement = require("../../utils/useRenderElement");
var _store2 = require("../store");
var _getPseudoElementBounds = require("../../utils/getPseudoElementBounds");
var _utils = require("../../floating-ui-react/utils");
var _mergeProps = require("../../merge-props");
var _useButton = require("../../use-button");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
const BOUNDARY_OFFSET = 2;
const customStyleHookMapping = {
..._popupStateMapping.pressableTriggerOpenStateMapping,
..._constants.fieldValidityMapping,
value: () => null
};
/**
* A button that opens the select menu.
* Renders a `<div>` element.
*
* Documentation: [Base UI Select](https://base-ui.com/react/components/select)
*/
const SelectTrigger = exports.SelectTrigger = /*#__PURE__*/React.forwardRef(function SelectTrigger(componentProps, forwardedRef) {
const {
render,
className,
disabled: disabledProp = false,
nativeButton = false,
...elementProps
} = componentProps;
const {
state: fieldState,
disabled: fieldDisabled
} = (0, _FieldRootContext.useFieldRootContext)();
const {
store,
setOpen,
selectionRef,
fieldControlValidation,
readOnly,
alignItemWithTriggerActiveRef,
disabled: selectDisabled,
keyboardActiveRef
} = (0, _SelectRootContext.useSelectRootContext)();
const disabled = fieldDisabled || selectDisabled || disabledProp;
const open = (0, _store.useStore)(store, _store2.selectors.open);
const value = (0, _store.useStore)(store, _store2.selectors.value);
const triggerProps = (0, _store.useStore)(store, _store2.selectors.triggerProps);
const positionerElement = (0, _store.useStore)(store, _store2.selectors.positionerElement);
const positionerRef = (0, _useLatestRef.useLatestRef)(positionerElement);
const {
labelId,
setTouched,
setFocused,
validationMode
} = (0, _FieldRootContext.useFieldRootContext)();
const triggerRef = React.useRef(null);
const timeoutFocus = (0, _useTimeout.useTimeout)();
const timeoutMouseDown = (0, _useTimeout.useTimeout)();
const {
getButtonProps,
buttonRef
} = (0, _useButton.useButton)({
disabled,
native: nativeButton
});
const setTriggerElement = (0, _useEventCallback.useEventCallback)(element => {
store.set('triggerElement', element);
});
const mergedRef = (0, _useMergedRefs.useMergedRefs)(forwardedRef, triggerRef, buttonRef, setTriggerElement);
const timeout1 = (0, _useTimeout.useTimeout)();
const timeout2 = (0, _useTimeout.useTimeout)();
React.useEffect(() => {
if (open) {
// mousedown -> move to unselected item -> mouseup should not select within 200ms.
timeout2.start(200, () => {
selectionRef.current.allowUnselectedMouseUp = true;
// mousedown -> mouseup on selected item should not select within 400ms.
timeout1.start(200, () => {
selectionRef.current.allowSelectedMouseUp = true;
});
});
return () => {
timeout1.clear();
timeout2.clear();
};
}
selectionRef.current = {
allowSelectedMouseUp: false,
allowUnselectedMouseUp: false
};
timeoutMouseDown.clear();
return undefined;
}, [open, selectionRef, timeoutMouseDown, timeout1, timeout2]);
const props = (0, _mergeProps.mergeProps)(triggerProps, {
'aria-labelledby': labelId,
'aria-readonly': readOnly || undefined,
tabIndex: disabled ? -1 : 0,
ref: mergedRef,
onFocus(event) {
setFocused(true);
// The popup element shouldn't obscure the focused trigger.
if (open && alignItemWithTriggerActiveRef.current) {
setOpen(false, (0, _createBaseUIEventDetails.createBaseUIEventDetails)('focus-out', event.nativeEvent));
}
// Saves a re-render on initial click: `forceMount === true` mounts
// the items before `open === true`. We could sync those cycles better
// without a timeout, but this is enough for now.
//
// XXX: might be causing `act()` warnings.
timeoutFocus.start(0, () => {
store.set('forceMount', true);
});
},
onBlur() {
setTouched(true);
setFocused(false);
if (validationMode === 'onBlur') {
fieldControlValidation.commitValidation(value);
}
},
onPointerMove({
pointerType
}) {
keyboardActiveRef.current = false;
store.set('touchModality', pointerType === 'touch');
},
onPointerDown({
pointerType
}) {
store.set('touchModality', pointerType === 'touch');
},
onKeyDown(event) {
keyboardActiveRef.current = true;
if (event.key === 'ArrowDown') {
setOpen(true, (0, _createBaseUIEventDetails.createBaseUIEventDetails)('list-navigation', event.nativeEvent));
}
},
onMouseDown(event) {
if (open) {
return;
}
const doc = (0, _owner.ownerDocument)(event.currentTarget);
function handleMouseUp(mouseEvent) {
if (!triggerRef.current) {
return;
}
const mouseUpTarget = mouseEvent.target;
// Early return if clicked on trigger element or its children
if ((0, _utils.contains)(triggerRef.current, mouseUpTarget) || (0, _utils.contains)(positionerRef.current, mouseUpTarget) || mouseUpTarget === triggerRef.current) {
return;
}
const bounds = (0, _getPseudoElementBounds.getPseudoElementBounds)(triggerRef.current);
if (mouseEvent.clientX >= bounds.left - BOUNDARY_OFFSET && mouseEvent.clientX <= bounds.right + BOUNDARY_OFFSET && mouseEvent.clientY >= bounds.top - BOUNDARY_OFFSET && mouseEvent.clientY <= bounds.bottom + BOUNDARY_OFFSET) {
return;
}
setOpen(false, (0, _createBaseUIEventDetails.createBaseUIEventDetails)('cancel-open', mouseEvent));
}
// Firefox can fire this upon mousedown
timeoutMouseDown.start(0, () => {
doc.addEventListener('mouseup', handleMouseUp, {
once: true
});
});
}
}, fieldControlValidation.getValidationProps, elementProps, getButtonProps);
// ensure nested useButton does not overwrite the combobox role:
// <Toolbar.Button render={<Select.Trigger />} />
props.role = 'combobox';
const state = React.useMemo(() => ({
...fieldState,
open,
disabled,
value,
readOnly
}), [fieldState, open, disabled, readOnly, value]);
return (0, _useRenderElement.useRenderElement)('div', componentProps, {
ref: [forwardedRef, triggerRef],
state,
customStyleHookMapping,
props
});
});
if (process.env.NODE_ENV !== "production") SelectTrigger.displayName = "SelectTrigger";
;