import { Select as Primitive } from "@base-ui/react/select"; import type { ReactNode } from "react"; import { mergeClassName, type StateClassName } from "./class-name.ts"; import { CheckGlyph, ChevronGlyph } from "./glyphs.tsx"; // The parts, for a select this wrapper's option list cannot express — groups, separators, // a scrollable list with arrows. export const SelectRoot = Primitive.Root; export const SelectTrigger = Primitive.Trigger; export const SelectValue = Primitive.Value; export const SelectIcon = Primitive.Icon; export const SelectPortal = Primitive.Portal; export const SelectPositioner = Primitive.Positioner; export const SelectPopup = Primitive.Popup; export const SelectList = Primitive.List; export const SelectItem = Primitive.Item; export const SelectItemIndicator = Primitive.ItemIndicator; export const SelectItemText = Primitive.ItemText; export const SelectGroup = Primitive.Group; export const SelectGroupLabel = Primitive.GroupLabel; export const SelectSeparator = Primitive.Separator; export interface SelectOption { value: T; label: string; disabled?: boolean; icon?: ReactNode; } export type SelectProps = Omit< Primitive.Root.Props, "children" | "items" | "defaultValue" | "multiple" > & { options?: readonly SelectOption[]; /** Shown while nothing is selected. No default because a default would be English. */ placeholder?: ReactNode; /** * Shown instead of the list when `options` is empty. Required, because a popup that opens * on nothing is a dead end: this is the line that says why it is empty. */ emptyLabel: ReactNode; /** * Label of an entry that puts the field back to nothing selected, for an optional question where * "I'd rather not say" has to stay reachable after the first answer. Rendered only once there is * a value to take back; picking it calls `onValueChange(null)`. */ clearLabel?: string; /** Accessible name for the trigger, when no visible label points at it. */ label?: string; /** Mark that says "this opens". */ chevron?: ReactNode; /** Mark next to the selected option. */ indicator?: ReactNode; /** Classes for the trigger button, which you see when the popup is closed. */ className?: StateClassName; popupClassName?: StateClassName; triggerProps?: Primitive.Trigger.Props; /** * Positioner props: `align`, `side`, `sideOffset`, `collisionPadding` and the stacking order. The * default `z-[60]` puts the list above the overlay layer (z-50: dialog, drawer); raise it with * `positionerProps={{ className: "z-[80]" }}` if the app stacks something higher than a dialog * above it. */ positionerProps?: Primitive.Positioner.Props; }; /** * A single-choice field. Trigger, portal, positioner, popup and list composed once; `name`, * `form`, `required`, `id`, `disabled` and every other root prop pass straight through to * Base UI's hidden input, so this submits inside a `
` like a native `