'use client'; import { Select } from '@base-ui/react/select'; import clsx from 'classnames'; import { useCallback } from 'react'; import { useStore } from 'zustand'; import { getOrCreateStoreByKey } from './getOrCreateStoreByKey'; import { OpenAPITooltip } from './OpenAPITooltip'; export type Key = string | number; export type OpenAPISelectItem = { key: Key; label: string | React.ReactNode; }; /** react-aria's `placement`, reduced to what the call sites actually use. */ type Placement = 'bottom start' | 'bottom end' | 'top start' | 'top end'; interface OpenAPISelectProps { items: T[]; /** A function, not children: the trigger has to render the selected item's markup too. */ children: (item: T) => React.ReactNode; className?: string; itemClassName?: string; placement?: Placement; stateKey?: string; value?: Key; defaultValue?: Key; onChange?: (key: Key) => void; /** * Icon to display in the select button. */ icon?: React.ReactNode | null; /** Owned here because only this component holds the trigger the tooltip anchors to. */ tooltip?: React.ReactNode; } export function useSelectState(stateKey = 'select-state', initialKey: Key = 'default') { const store = useStore(getOrCreateStoreByKey(stateKey, initialKey)); return { key: store.key, setKey: useCallback((key: Key | null) => store.setKey(key), [store.setKey]), }; } export function OpenAPISelect(props: OpenAPISelectProps) { const { icon, items, children, className, itemClassName, placement, stateKey, value, onChange, defaultValue, tooltip, } = props; const state = useSelectState(stateKey, defaultValue ?? items[0]?.key); const selected = items.find((item) => item.key === state.key) || items[0]; const [side, align] = (placement ?? 'bottom start').split(' ') as [ 'bottom' | 'top', 'start' | 'end', ]; // An element, not a component: declaring one inline would give React a new type on every // render and remount the whole trigger each time. const trigger = ( {(current) => { const item = items.find((entry) => entry.key === current) ?? selected; return item ? children(item) : null; }} {icon !== null ? icon || '▼' : null} ); return ( value={value ?? selected?.key} onValueChange={(key) => { onChange?.(key as Key); state.setKey(key as Key); }} // The select sits inside a document, so it must not lock scrolling behind a modal layer. modal={false} >
{tooltip ? ( {tooltip} ) : ( trigger )}
{/* Base UI scrolls and reveals the selected item through the list, not the popup. */} {items.map((item) => ( {children(item)} ))} ); } export function OpenAPISelectItem(props: { value: Key; children: React.ReactNode; className?: string; }) { const { value, children, className } = props; return ( clsx( 'openapi-select-item', { 'openapi-select-item-focused': highlighted, 'openapi-select-item-selected': selected, }, className ) } > {children} ); }