/** * The single-choice picker (milestone 23, ruling 4), replacing every native * ``'s value was a string. A call site that * models an id converts on both edges. */ import { CaretDown } from "@phosphor-icons/react/dist/icons/CaretDown"; import * as stylex from "@stylexjs/stylex"; import { Button, Label, ListBox, ListBoxItem, Popover, Select as AriaSelect, SelectValue, Text, } from "react-aria-components"; import { colors, metrics } from "./tokens.stylex"; import { styles as shared } from "./styles"; export interface SelectOption { value: string; label: string; } interface Props { options: readonly SelectOption[]; value: string; onChange: (value: string) => void; /** The visible label. Omit it only when `aria-label` names the control. */ label?: string; "aria-label"?: string; /** * Text under the control that qualifies it. RAC gives it an id and points the * trigger's `aria-describedby` at it, which a sibling rendered by the caller * cannot be: the trigger is this file's, so only this file can name it. */ description?: string; /** * `field` matches a full-width form input, `compactField` the smaller one a * dialog uses, `inline` a control sitting in a row of other controls, and * `toolbar` the 44px hairline dropdown a page's scope picker is. */ variant?: "field" | "compactField" | "inline" | "toolbar"; /** Visible but inert, keeping its value on screen; RAC also drops it from the tab order. */ isDisabled?: boolean; } const styles = stylex.create({ root: { display: "block", }, label: { display: "block", fontSize: "0.875rem", lineHeight: "1.25rem", fontWeight: 500, }, trigger: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", textAlign: "left", // RAC renders a real ` {description !== undefined && ( {description} )} stylex.props(styles.popover).className ?? ""}> {options.map((option) => ( stylex.props( styles.item, shared.insetFocusRing, isSelected && styles.itemSelected, isFocused && styles.itemFocused, ).className ?? "" } > {option.label} ))} ); }