import "./select.css"; import type * as React from "react"; import { type ReactNode } from "react"; import { Combobox } from "@base-ui/react/combobox"; import { useRender } from "@base-ui/react/use-render"; import type { Controlled, SelfCommitting } from "./commit_mode"; import { type FieldOptionsSource } from "./option_picker"; import { type StyleProps } from "./style_props"; export interface SelectOption { label?: string; value: T; disabled?: boolean; testID?: string; /** Arbitrary payload, so a rich `renderOptionContent` renders from the source * row without a side lookup. */ data?: D; } export type SelectValue = MULTI extends true ? T[] : T; /** What a picker ANSWERS with — the single arm answers `null` for nothing * chosen, never the empty string, a value a field could legitimately hold. */ export type SelectAnswer = MULTI extends true ? T[] : T | null; export type SelectOnValueChange = (value: SelectAnswer) => void; interface SelectBase extends StyleProps { /** {@link SelectNativeProps} is the other dropdown; this arm is the kit's own. */ native?: never; options?: (SelectOption | undefined | false)[]; /** A SELECT FIELD, straight from the SDK (`useFieldOptions().fields.`) — * its whole option set becomes this picker's options, keys and colours intact. * `options` wins where a caller states both. */ fieldOptions?: FieldOptionsSource; /** DRAW EACH OF THE FIELD'S OPTIONS AS ITS OWN MARK — `dot` in a list of * choices, `tonal` where the value is the surface's one prominent status. */ badge?: "tonal" | "dot"; /** How each OPTION renders in the menu, and the trigger's selection display * where `renderSelected` is omitted. */ renderOptionContent?: (option: SelectOption) => ReactNode; /** A single-line subtitle under each option's label in the menu. */ getOptionDescription?: (option: SelectOption) => string | undefined; /** A NODE renders as-is; a string is drawn as muted placeholder text. */ placeholder?: string | ReactNode; /** What the LIST says when it has nothing to offer. Default: "No results". */ emptyText?: string; /** THE READER MAY EMPTY THIS FIELD — single-select only, as a named row * (`emptyLabel`) at the head of the list. */ clearable?: boolean; /** The name of that empty row (locale default: "None"), as distinct from * `placeholder`. */ emptyLabel?: string; searchable?: boolean; /** Worth setting under `allowCustom`: the only always-visible place that can * say a new value is allowed. */ searchPlaceholder?: string; /** THE CALLER NARROWS THE SET: the query arrives debounced and `options` is the * answer to it. Implies `searchable`. */ onSearchChange?: (query: string) => void; /** Offer a "create" row when the query matches no option. Implies `searchable`: * there has to be a query to create from. */ allowCustom?: boolean; /** Label for the create row (default: `Add ""`). */ customOptionLabel?: (query: string) => string | null; /** Where the create row sits — default "bottom". Pass "top" for a find-or-create * REFERENCE picker over a long registry: the keyboard highlight stays on the * first MATCH, so Enter on a partial query attaches rather than duplicating. */ customOptionPlacement?: "top" | "bottom"; /** WHICH BAND this select sits in: `lg` (default) is the 40px control height, * `md` the DENSE strip of a document toolbar. */ size?: "md" | "lg"; disabled?: boolean; /** Focus the field on mount; to open the LIST on mount pass `defaultOpen`. */ autoFocus?: boolean; defaultOpen?: boolean; /** Required in spirit with no visible label beside the field: the selected text * describes the value, not the control. */ accessibilityLabel?: string; /** On the TRIGGER — the element a test presses to open the list. */ testID?: string; /** Base UI's own trigger ref. A `
`, not a `