/** @jsxImportSource react */ import { useCallback, useImperativeHandle, useRef, useState, type CSSProperties } from "react"; import { useThemeColors } from "../../../../theme/theme-context"; import { blendHex } from "../../../../theme/colors"; import { WEB_CELL_HEIGHT } from "../../../../theme/font-scale"; import { Box } from "../../../../ui"; import { MenuPopover } from "../../../../components/ui/menu"; import type { SelectFieldHandle, SelectFieldProps } from "../../../../components/ui/select-field"; import { WebIcon } from "./icons"; import { isInsideDialogSurface } from "../host/focus-scope"; const UNSET_VALUE = "\u0000unset"; function Chevron() { return ( ); } /** Desktop host for the kit SelectField: a trigger that opens the kit Menu. */ export function WebSelectField({ label, value, options, width, height, variant = "field", includeUnsetOption = false, restoreFocus = true, disabled = false, selectRef, controlRef, onFocus, onChange, }: SelectFieldProps) { const colors = useThemeColors(); const [open, setOpen] = useState(false); const buttonRef = useRef(null); const inline = variant === "inline"; const current = options.find((option) => option.value === value); const showUnset = includeUnsetOption && !current; const openMenu = useCallback(() => { if (disabled) return; onFocus?.(); setOpen(true); }, [disabled, onFocus]); const handle = useRef({ open: () => {}, focus: () => {} }); handle.current.open = openMenu; handle.current.focus = () => buttonRef.current?.focus(); useImperativeHandle(controlRef, () => handle.current, []); const setSelectRef = useRef(selectRef); setSelectRef.current = selectRef; const resolvedWidth = width ?? (inline ? "auto" : 184); const resolvedHeight = height ?? (inline ? WEB_CELL_HEIGHT : 28); const style: CSSProperties = { width: resolvedWidth, height: resolvedHeight, display: "inline-flex", alignItems: "center", justifyContent: "space-between", gap: 6, color: disabled ? colors.textMuted : colors.text, backgroundColor: inline ? "transparent" : blendHex(colors.panel, colors.textBright, 0.06), border: inline ? "none" : `1px solid ${open ? colors.borderFocused : colors.border}`, borderRadius: inline ? 0 : 6, padding: inline ? 0 : "0 8px", boxShadow: inline ? "none" : `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.05)}`, cursor: disabled ? "default" : "pointer", font: "inherit", textAlign: "left", boxSizing: "border-box", }; const trigger = ( ); return ( { event.stopPropagation?.(); }} onMouseUp={(event: any) => { event.stopPropagation?.(); }} > { setOpen(next); // Back to the trigger inside a dialog, where Tab walks the fields. In a // pane the trigger would keep taking Enter from the rows around it. if (!next && restoreFocus && isInsideDialogSurface(buttonRef.current)) { queueMicrotask(() => buttonRef.current?.focus({ preventScroll: true })); } }} trigger={trigger} label={label} selection="single" minWidth={typeof resolvedWidth === "number" ? Math.max(160, resolvedWidth) : 170} items={[ ...(showUnset ? [{ id: UNSET_VALUE, label: "Unset", selected: true, disabled: true }] : []), ...options.map((option) => ({ id: option.value, label: option.label, description: option.description, disabled: option.disabled, selected: option.value === value, })), ]} onSelect={(id) => { if (id !== UNSET_VALUE && id !== value) onChange(id); }} /> ); }