UNPKG

@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.

207 lines (204 loc) 6.82 kB
'use client'; import * as React from 'react'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { useLatestRef } from '@base-ui-components/utils/useLatestRef'; import { isMouseWithinBounds } from '@base-ui-components/utils/isMouseWithinBounds'; import { useStore } from '@base-ui-components/utils/store'; import { useSelectRootContext } from "../root/SelectRootContext.js"; import { useCompositeListItem, IndexGuessBehavior } from "../../composite/list/useCompositeListItem.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { SelectItemContext } from "./SelectItemContext.js"; import { selectors } from "../store.js"; import { useButton } from "../../use-button/index.js"; /** * An individual option in the select menu. * Renders a `<div>` element. * * Documentation: [Base UI Select](https://base-ui.com/react/components/select) */ import { jsx as _jsx } from "react/jsx-runtime"; export const SelectItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function SelectItem(componentProps, forwardedRef) { const { render, className, value = null, label, disabled = false, nativeButton = false, ...elementProps } = componentProps; const textRef = React.useRef(null); const listItem = useCompositeListItem({ label, textRef, indexGuessBehavior: IndexGuessBehavior.GuessFromOrder }); const { store, getItemProps, setOpen, setValue, selectionRef, typingRef, valuesRef, registerItemIndex, keyboardActiveRef, highlightTimeout, multiple } = useSelectRootContext(); const highlighted = useStore(store, selectors.isActive, listItem.index); const selected = useStore(store, selectors.isSelected, listItem.index, value); const rootValue = useStore(store, selectors.value); const selectedByFocus = useStore(store, selectors.isSelectedByFocus, listItem.index); const itemRef = React.useRef(null); const indexRef = useLatestRef(listItem.index); const hasRegistered = listItem.index !== -1; useIsoLayoutEffect(() => { if (!hasRegistered) { return undefined; } const values = valuesRef.current; values[listItem.index] = value; return () => { delete values[listItem.index]; }; }, [hasRegistered, listItem.index, value, valuesRef]); useIsoLayoutEffect(() => { if (hasRegistered) { if (multiple) { const isValueSelected = Array.isArray(rootValue) && rootValue.includes(value); if (isValueSelected) { registerItemIndex(listItem.index); } } else if (value === rootValue) { registerItemIndex(listItem.index); } } }, [hasRegistered, listItem.index, registerItemIndex, value, rootValue, multiple]); const state = React.useMemo(() => ({ disabled, selected, highlighted }), [disabled, selected, highlighted]); const rootProps = getItemProps({ active: highlighted, selected }); // With our custom `focusItemOnHover` implementation, this interferes with the logic and can // cause the index state to be stuck when leaving the select popup. delete rootProps.onFocus; delete rootProps.id; const lastKeyRef = React.useRef(null); const pointerTypeRef = React.useRef('mouse'); const didPointerDownRef = React.useRef(false); const { getButtonProps, buttonRef } = useButton({ disabled, focusableWhenDisabled: true, native: nativeButton }); function commitSelection(event) { if (multiple) { const currentValue = Array.isArray(rootValue) ? rootValue : []; const nextValue = selected ? currentValue.filter(v => v !== value) : [...currentValue, value]; setValue(nextValue, event); } else { setValue(value, event); setOpen(false, event, 'item-press'); } } const defaultProps = { 'aria-disabled': disabled || undefined, tabIndex: highlighted ? 0 : -1, onFocus() { store.set('activeIndex', indexRef.current); }, onMouseEnter() { if (!keyboardActiveRef.current && store.state.selectedIndex === null) { store.set('activeIndex', indexRef.current); } }, onMouseMove() { store.set('activeIndex', indexRef.current); }, onMouseLeave(event) { if (keyboardActiveRef.current || isMouseWithinBounds(event)) { return; } highlightTimeout.start(0, () => { if (store.state.activeIndex === indexRef.current) { store.set('activeIndex', null); } }); }, onTouchStart() { selectionRef.current = { allowSelectedMouseUp: false, allowUnselectedMouseUp: false, allowSelect: true }; }, onKeyDown(event) { selectionRef.current.allowSelect = true; lastKeyRef.current = event.key; store.set('activeIndex', indexRef.current); }, onClick(event) { didPointerDownRef.current = false; // Prevent double commit on {Enter} if (event.type === 'keydown' && lastKeyRef.current === null) { return; } if (disabled || lastKeyRef.current === ' ' && typingRef.current || pointerTypeRef.current !== 'touch' && !highlighted) { return; } if (selectionRef.current.allowSelect) { lastKeyRef.current = null; commitSelection(event.nativeEvent); } }, onPointerEnter(event) { pointerTypeRef.current = event.pointerType; }, onPointerDown(event) { pointerTypeRef.current = event.pointerType; didPointerDownRef.current = true; }, onMouseUp(event) { if (disabled) { return; } if (didPointerDownRef.current) { didPointerDownRef.current = false; return; } const disallowSelectedMouseUp = !selectionRef.current.allowSelectedMouseUp && selected; const disallowUnselectedMouseUp = !selectionRef.current.allowUnselectedMouseUp && !selected; if (disallowSelectedMouseUp || disallowUnselectedMouseUp || pointerTypeRef.current !== 'touch' && !highlighted) { return; } if (selectionRef.current.allowSelect || !selected) { commitSelection(event.nativeEvent); } selectionRef.current.allowSelect = true; } }; const element = useRenderElement('div', componentProps, { ref: [buttonRef, forwardedRef, listItem.ref, itemRef], state, props: [rootProps, defaultProps, elementProps, getButtonProps] }); const contextValue = React.useMemo(() => ({ selected, indexRef, textRef, selectedByFocus }), [selected, indexRef, textRef, selectedByFocus]); return /*#__PURE__*/_jsx(SelectItemContext.Provider, { value: contextValue, children: element }); })); if (process.env.NODE_ENV !== "production") SelectItem.displayName = "SelectItem";