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.

173 lines (170 loc) 6.2 kB
'use client'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { useStore } from '@base-ui-components/utils/store'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { useComboboxRootContext, useComboboxDerivedItemsContext } from "../root/ComboboxRootContext.js"; import { useCompositeListItem, IndexGuessBehavior } from "../../composite/list/useCompositeListItem.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { ComboboxItemContext } from "./ComboboxItemContext.js"; import { selectors } from "../store.js"; import { useButton } from "../../use-button/index.js"; import { useComboboxRowContext } from "../row/ComboboxRowContext.js"; import { compareItemEquality, findItemIndex } from "../../utils/itemEquality.js"; /** * An individual item in the list. * Renders a `<div>` element. */ import { jsx as _jsx } from "react/jsx-runtime"; export const ComboboxItem = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(function ComboboxItem(componentProps, forwardedRef) { const { render, className, value = null, index: indexProp, disabled = false, nativeButton = false, ...elementProps } = componentProps; const didPointerDownRef = React.useRef(false); const textRef = React.useRef(null); const listItem = useCompositeListItem({ index: indexProp, textRef, indexGuessBehavior: IndexGuessBehavior.GuessFromOrder }); const store = useComboboxRootContext(); const isRow = useComboboxRowContext(); const { flatFilteredItems } = useComboboxDerivedItemsContext(); const open = useStore(store, selectors.open); const selectionMode = useStore(store, selectors.selectionMode); const readOnly = useStore(store, selectors.readOnly); const virtualized = useStore(store, selectors.virtualized); const isItemEqualToValue = useStore(store, selectors.isItemEqualToValue); const selectable = selectionMode !== 'none'; const index = indexProp ?? (virtualized ? findItemIndex(flatFilteredItems, value, isItemEqualToValue) : listItem.index); const hasRegistered = listItem.index !== -1; const rootId = useStore(store, selectors.id); const highlighted = useStore(store, selectors.isActive, index); const matchesSelectedValue = useStore(store, selectors.isSelected, value); const items = useStore(store, selectors.items); const getItemProps = useStore(store, selectors.getItemProps); const itemRef = React.useRef(null); const id = rootId != null && hasRegistered ? `${rootId}-${index}` : undefined; const selected = matchesSelectedValue && selectable; useIsoLayoutEffect(() => { const shouldRun = hasRegistered && (virtualized || indexProp != null); if (!shouldRun) { return undefined; } const list = store.state.listRef.current; list[index] = itemRef.current; return () => { delete list[index]; }; }, [hasRegistered, virtualized, index, indexProp, store]); useIsoLayoutEffect(() => { if (!hasRegistered || items) { return undefined; } const visibleMap = store.state.valuesRef.current; visibleMap[index] = value; // Stable registry that doesn't depend on filtering. Assume that no // filtering had occurred at this point; otherwise, an `items` prop is // required. if (selectionMode !== 'none') { store.state.allValuesRef.current.push(value); } return () => { delete visibleMap[index]; }; }, [hasRegistered, items, index, value, store, selectionMode]); useIsoLayoutEffect(() => { if (!open) { didPointerDownRef.current = false; return; } if (!hasRegistered || items) { return; } const selectedValue = store.state.selectedValue; const lastSelectedValue = Array.isArray(selectedValue) ? selectedValue[selectedValue.length - 1] : selectedValue; if (compareItemEquality(lastSelectedValue, value, isItemEqualToValue)) { store.set('selectedIndex', index); } }, [hasRegistered, items, open, store, index, value, isItemEqualToValue]); const state = React.useMemo(() => ({ disabled, selected, highlighted }), [disabled, selected, highlighted]); const rootProps = getItemProps({ active: highlighted, selected }); rootProps.id = undefined; rootProps.onFocus = undefined; const { getButtonProps, buttonRef } = useButton({ disabled, focusableWhenDisabled: true, native: nativeButton }); function commitSelection(nativeEvent) { function selectItem() { store.state.handleSelection(nativeEvent, value); } if (store.state.submitOnItemClick) { ReactDOM.flushSync(selectItem); store.state.requestSubmit(); } else { selectItem(); } } const defaultProps = { id, role: isRow ? 'gridcell' : 'option', 'aria-disabled': disabled || undefined, 'aria-selected': selectable ? selected : undefined, // Focusable items steal focus from the input upon mouseup. // Warn if the user renders a natively focusable element like `<button>`, // as it should be a `<div>` instead. tabIndex: undefined, onPointerDownCapture(event) { didPointerDownRef.current = true; event.preventDefault(); }, onClick(event) { if (disabled || readOnly) { return; } commitSelection(event.nativeEvent); }, onMouseUp(event) { const pointerStartedOnItem = didPointerDownRef.current; didPointerDownRef.current = false; if (disabled || readOnly || event.button !== 0 || pointerStartedOnItem || !highlighted) { return; } commitSelection(event.nativeEvent); } }; const element = useRenderElement('div', componentProps, { ref: [buttonRef, forwardedRef, listItem.ref, itemRef], state, props: [rootProps, defaultProps, elementProps, getButtonProps] }); const contextValue = React.useMemo(() => ({ selected, textRef }), [selected, textRef]); return /*#__PURE__*/_jsx(ComboboxItemContext.Provider, { value: contextValue, children: element }); })); if (process.env.NODE_ENV !== "production") ComboboxItem.displayName = "ComboboxItem";