ComboBox 组合框
将文本输入与 ListBox 结合,用户可通过输入查询把选项列表过滤为匹配项
用法
import { ComboBox } from '@lenso/ui';此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native Base UI adaptation.import { AnimalPicker } from "./shared";export function Default() { return <AnimalPicker />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件结构
import { ComboBox, Input, Label, Description, Header, ListBox, Separator } from '@lenso/ui';
export default () => ( <ComboBox> <Label /> <ComboBox.InputGroup> <Input /> <ComboBox.Trigger /> </ComboBox.InputGroup> {/* 展示已选中的值,主要用于多选 */} <ComboBox.Value /> <Description /> <ComboBox.Popover> <ListBox> <ListBox.Item> <Label /> <Description /> <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Section> <Header /> <ListBox.Item> <Label /> </ListBox.Item> </ListBox.Section> </ListBox> </ComboBox.Popover> </ComboBox>)示例
宽度充满
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import * as stylex from "@stylexjs/stylex";import { AnimalPicker, animals } from "./shared";import { styles } from "./styles.stylex";export function FullWidth() { return ( <div {...stylex.props(styles.wide)}> <AnimalPicker fullWidth items={animals.slice(0, 3)} /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
带描述
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { AnimalPicker } from "./shared";export function WithDescription() { return <AnimalPicker description="Search and select your favorite animal" />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
必填
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Base Field context owns validation and error linkage.import { Button, FieldError, Form, TextField } from "@lenso/ui";import { AnimalPicker } from "./shared";import { styles } from "./styles.stylex";
export function AnimalForm({ secondary = false }: { secondary?: boolean }) { return ( <Form xstyle={[styles.form, secondary && styles.fullWidth]} onSubmit={(event) => { event.preventDefault(); new FormData(event.currentTarget); alert("Form submitted successfully!"); }} > <TextField name="animal"> <AnimalPicker required name="animal" fullWidth secondary={secondary} /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> );}export function Required() { return <AnimalForm />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
禁用
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { AnimalPicker, animals } from "./shared";export function Disabled() { return <AnimalPicker disabled defaultValue={animals[1]} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
含禁用选项
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { AnimalPicker } from "./shared";const animals = [ { id: "dog", name: "Dog" }, { id: "cat", name: "Cat" }, { id: "bird", name: "Bird" }, { id: "kangaroo", name: "Kangaroo" }, { id: "elephant", name: "Elephant" }, { id: "tiger", name: "Tiger" },];export function WithDisabledOptions() { return <AnimalPicker label="Animal" items={animals} disabledIds={["cat", "kangaroo"]} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
分组选项
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native grouped collection filters without losing section semantics.import { ComboBox } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useId } from "react";import { styles } from "./styles.stylex";import { useFocusMenu } from "./shared";const regions = [ { label: "North America", items: [ { value: "usa", label: "United States" }, { value: "canada", label: "Canada" }, { value: "mexico", label: "Mexico" }, ], }, { label: "Europe", items: [ { value: "uk", label: "United Kingdom" }, { value: "france", label: "France" }, { value: "germany", label: "Germany" }, { value: "spain", label: "Spain" }, { value: "italy", label: "Italy" }, ], }, { label: "Asia", items: [ { value: "japan", label: "Japan" }, { value: "china", label: "China" }, { value: "india", label: "India" }, { value: "south-korea", label: "South Korea" }, ], },];export function WithSections() { const id = useId(); const menu = useFocusMenu(); return ( <div {...stylex.props(styles.field)}> <ComboBox {...menu.root} items={regions}> <ComboBox.Label htmlFor={id}>Country</ComboBox.Label> <ComboBox.InputGroup> <ComboBox.Input {...menu.input} id={id} placeholder="Search countries..." /> <ComboBox.Trigger aria-label="Show countries"> <ComboBox.Indicator /> </ComboBox.Trigger> </ComboBox.InputGroup> <ComboBox.Portal> <ComboBox.Positioner> <ComboBox.Popover> <ComboBox.List> {(region: (typeof regions)[number], index: number) => ( <ComboBox.Group key={region.label} items={region.items}> {index > 0 && <ComboBox.Separator />} <ComboBox.GroupLabel>{region.label}</ComboBox.GroupLabel> <ComboBox.Collection> {(country: (typeof region.items)[number]) => ( <ComboBox.Item key={country.value} value={country}> {country.label} <ComboBox.ItemIndicator /> </ComboBox.Item> )} </ComboBox.Collection> </ComboBox.Group> )} </ComboBox.List> </ComboBox.Popover> </ComboBox.Positioner> </ComboBox.Portal> </ComboBox> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控组件
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { AnimalPicker, smallAnimals, type Animal } from "./shared";import { styles } from "./styles.stylex";export function Controlled() { const [selectedAnimal, setSelectedAnimal] = useState<Animal | null>(smallAnimals[0] ?? null); return ( <div {...stylex.props(styles.column)}> <AnimalPicker label="Animal (controlled)" items={smallAnimals} value={selectedAnimal} onValueChange={setSelectedAnimal} /> <p {...stylex.props(styles.muted)}>Selected: {selectedAnimal?.name || "None"}</p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控输入值
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { AnimalPicker } from "./shared";import { styles } from "./styles.stylex";export function ControlledInputValue() { const [inputValue, setInputValue] = useState(""); return ( <div {...stylex.props(styles.column)}> <AnimalPicker label="Search (controlled input)" placeholder="Type to search..." inputValue={inputValue} onInputValueChange={setInputValue} /> <p {...stylex.props(styles.muted)}>Input value: {inputValue || "(empty)"}</p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
异步加载
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** * HeroUI v3.2.6, Apache-2.0. * Modified: abortable fetch and an intersection sentinel replace React Stately's async collection. */import { ComboBox, Spinner } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useEffect, useId, useRef, useState } from "react";import { styles } from "./styles.stylex";import { useFocusMenu } from "./shared";
interface Character { name: string;}interface Page { next: string | null; results: Character[];}export function AsynchronousLoading() { const id = useId(); const menu = useFocusMenu(); const [inputValue, setInputValue] = useState(""); const [items, setItems] = useState<Character[]>([]); const [cursor, setCursor] = useState<string | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [nextPage, setNextPage] = useState<string | null>(null); const [sentinel, setSentinel] = useState<HTMLDivElement | null>(null); const generation = useRef(0);
useEffect(() => { const controller = new AbortController(); const current = ++generation.current; setItems([]); setCursor(null); setNextPage(null); setLoading(true); setError(null); fetch(`https://swapi.py4e.com/api/people/?search=${encodeURIComponent(inputValue)}`, { signal: controller.signal, }) .then(async (response) => { if (!response.ok) throw new Error(`Request failed (${response.status})`); return (await response.json()) as Page; }) .then((page) => { if (current !== generation.current) return; setItems(page.results); setCursor(page.next); }) .catch((reason: unknown) => { if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : "Unable to load characters"); }) .finally(() => { if (current === generation.current && !controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [inputValue]);
useEffect(() => { if (!nextPage) return; const controller = new AbortController(); const current = generation.current; setLoading(true); setError(null); fetch(nextPage.replace(/^http:\/\//i, "https://"), { signal: controller.signal }) .then(async (response) => { if (!response.ok) throw new Error(`Request failed (${response.status})`); return (await response.json()) as Page; }) .then((page) => { if (current !== generation.current) return; setItems((previous) => [...previous, ...page.results]); setCursor(page.next); }) .catch((reason: unknown) => { if (!controller.signal.aborted) setError(reason instanceof Error ? reason.message : "Unable to load characters"); }) .finally(() => { if (current === generation.current && !controller.signal.aborted) { setLoading(false); setNextPage(null); } }); return () => controller.abort(); }, [nextPage]);
useEffect(() => { if (!sentinel || !cursor || loading || error) return; const observer = new IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) setNextPage(cursor); }); observer.observe(sentinel); return () => observer.disconnect(); }, [sentinel, cursor, loading, error]);
return ( <div {...stylex.props(styles.field)}> <ComboBox<Character> {...menu.root} items={items} filter={null} inputValue={inputValue} onInputValueChange={setInputValue} itemToStringLabel={(character) => character.name} itemToStringValue={(character) => character.name} > <ComboBox.Label htmlFor={id}>Pick a Character</ComboBox.Label> <ComboBox.InputGroup> <ComboBox.Input {...menu.input} id={id} placeholder="Star Wars characters..." /> <ComboBox.Trigger aria-label="Show characters"> <ComboBox.Indicator /> </ComboBox.Trigger> </ComboBox.InputGroup> <ComboBox.Portal> <ComboBox.Positioner> <ComboBox.Popover> <ComboBox.List aria-busy={loading}> {(character: Character) => ( <ComboBox.Item key={character.name} value={character}> {character.name} <ComboBox.ItemIndicator /> </ComboBox.Item> )} </ComboBox.List> <ComboBox.Empty> {loading ? "Loading..." : (error ?? "No results found")} </ComboBox.Empty> <div ref={setSentinel} {...stylex.props(styles.loading)}> {loading && ( <> <Spinner size="sm" /> <span {...stylex.props(styles.muted)}> {items.length ? "Loading more..." : "Loading..."} </span> </> )} {cursor && !loading && ( <button type="button" onClick={() => setNextPage(cursor)}> Load more </button> )} {error && items.length > 0 && <span role="alert">{error}</span>} </div> </ComboBox.Popover> </ComboBox.Positioner> </ComboBox.Portal> </ComboBox> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
默认选中项
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native defaultValue replaces defaultSelectedKey.import { AnimalPicker, animals } from "./shared";export function DefaultSelectedKey() { return <AnimalPicker defaultValue={animals[1]} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
允许自定义值
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native input retains free text independently of selection.import { AnimalPicker } from "./shared";export function AllowsCustomValue() { return ( <AnimalPicker placeholder="Search or type an animal..." description="You can type any animal name, even if it's not in the list" /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义指示器
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { ChevronsExpandVertical } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { AnimalPicker } from "./shared";import { styles } from "./styles.stylex";export function CustomIndicator() { return ( <AnimalPicker indicator={<ChevronsExpandVertical aria-hidden {...stylex.props(styles.icon)} />} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义展示值
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Source user data and avatar composition retained.import { Avatar, AvatarImage, AvatarFallback, ComboBox } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useId } from "react";import { styles } from "./styles.stylex";import { useFocusMenu } from "./shared";const users = [ { avatarUrl: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg", email: "[email protected]", fallback: "B", id: "1", name: "Bob", }, { avatarUrl: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg", email: "[email protected]", fallback: "F", id: "2", name: "Fred", }, { avatarUrl: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg", email: "[email protected]", fallback: "M", id: "3", name: "Martha", }, { avatarUrl: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg", email: "[email protected]", fallback: "J", id: "4", name: "John", }, { avatarUrl: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg", email: "[email protected]", fallback: "J", id: "5", name: "Jane", },];export function CustomValue() { const id = useId(); const menu = useFocusMenu(); return ( <div {...stylex.props(styles.field)}> <ComboBox<(typeof users)[number]> {...menu.root} items={users} itemToStringLabel={(user) => user.name} itemToStringValue={(user) => user.id} > <ComboBox.Label htmlFor={id}>User</ComboBox.Label> <ComboBox.InputGroup> <ComboBox.Input {...menu.input} id={id} placeholder="Search users..." /> <ComboBox.Trigger aria-label="Show users"> <ComboBox.Indicator /> </ComboBox.Trigger> </ComboBox.InputGroup> <ComboBox.Portal> <ComboBox.Positioner> <ComboBox.Popover> <ComboBox.List> {(user: (typeof users)[number]) => ( <ComboBox.Item key={user.id} value={user}> <Avatar size="sm"> <AvatarImage src={user.avatarUrl} alt="" /> <AvatarFallback>{user.fallback}</AvatarFallback> </Avatar> <div {...stylex.props(styles.user)}> <span>{user.name}</span> <span {...stylex.props(styles.muted)}>{user.email}</span> </div> <ComboBox.ItemIndicator /> </ComboBox.Item> )} </ComboBox.List> </ComboBox.Popover> </ComboBox.Positioner> </ComboBox.Portal> </ComboBox> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义过滤
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { AnimalPicker, smallAnimals } from "./shared";export function CustomFiltering() { return ( <AnimalPicker label="Animal (custom filter)" items={smallAnimals} filter={(animal, inputValue) => !inputValue || animal.name.toLowerCase().includes(inputValue.toLowerCase()) } /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
渲染函数
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Base UI Root is non-DOM; compose its native InputGroup.import { AnimalPicker } from "./shared";export function RenderFunction() { return <AnimalPicker composed />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
菜单触发方式
使用 menuTrigger prop 控制 Popover 何时打开:
focus(默认):输入框获得焦点时打开 Popoverinput:用户编辑输入文本时打开 Popovermanual:仅当用户按下触发按钮或使用方向键时打开 Popover
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Opening policies use native controlled open events.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { AnimalPicker } from "./shared";import { styles } from "./styles.stylex";
function Policy({ mode }: { mode: "focus" | "input" | "manual" }) { const [open, setOpen] = useState(false); const description = { focus: "Popover opens when the input is focused", input: "Popover opens when the user edits the input text", manual: "Popover only opens when the trigger button is pressed or arrow keys are used", }; return ( <div {...stylex.props(styles.column)}> <p {...stylex.props(styles.caption)}> {mode === "focus" ? "Focus (default)" : mode === "input" ? "Input" : "Manual"} </p> <AnimalPicker open={open} openOnInputClick={mode === "focus"} inputFocus={mode === "focus" ? () => setOpen(true) : undefined} onOpenChange={(next, details) => { if (next && mode === "manual" && details.reason === "input-change") return; setOpen(next); }} description={description[mode]} /> </div> );}export function MenuTrigger() { return ( <div {...stylex.props(styles.menu)}> <Policy mode="focus" /> <Policy mode="input" /> <Policy mode="manual" /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
多选
设置 selectionMode="multiple" 以允许选择多个选项。在多选模式下,使用 ComboBox.Value 展示已选项,并同时给内部的 ListBox 传入 selectionMode="multiple"。选择通过 value / defaultValue(Key[])以及 onChange 回调来控制。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native chips preserve keyboard removal and focus.import { ComboBox } from "@lenso/ui";import { useId } from "react";import * as stylex from "@stylexjs/stylex";import { animals, animalLabel, animalValue, sameAnimal, AnimalOptions, type Animal, useFocusMenu,} from "./shared";import { styles } from "./styles.stylex";
export function MultipleSelection() { const id = useId(); const menu = useFocusMenu(); return ( <div {...stylex.props(styles.field)}> <ComboBox {...menu.root} multiple items={animals} itemToStringLabel={animalLabel} itemToStringValue={animalValue} isItemEqualToValue={sameAnimal} > <ComboBox.Label htmlFor={id}>Favorite Animals</ComboBox.Label> <ComboBox.InputGroup> <ComboBox.Input {...menu.input} id={id} placeholder="Search animals..." /> <ComboBox.Trigger aria-label="Show animals"> <ComboBox.Indicator /> </ComboBox.Trigger> </ComboBox.InputGroup> <ComboBox.Chips> <ComboBox.Value> {(selected: Animal[]) => selected.length === 0 ? ( <span {...stylex.props(styles.muted)}>No animals selected</span> ) : ( selected.map((animal) => ( <ComboBox.Chip key={animal.id}> {animal.name} <ComboBox.ChipRemove aria-label={`Remove ${animal.name}`}> × </ComboBox.ChipRemove> </ComboBox.Chip> )) ) } </ComboBox.Value> </ComboBox.Chips> <AnimalOptions /> </ComboBox> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
表面样式
在 Surface 内使用时,请使用 variant="secondary" 以应用适合 Surface 背景的低强调变体。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0.import { Surface } from "@lenso/ui";import { AnimalForm } from "./required";import { styles } from "./styles.stylex";export function OnSurface() { return ( <Surface xstyle={styles.surface}> <AnimalForm secondary /> </Surface> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义样式
Tailwind CSS
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// HeroUI v3.2.6, Apache-2.0. Native highlighted/selected states replace RAC selectors.import { ComboBox } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useId } from "react";import { styles } from "./styles.stylex";import { useFocusMenu } from "./shared";const frameworks = [ { value: "react", label: "React" }, { value: "vue", label: "Vue" }, { value: "svelte", label: "Svelte" },];export function CustomStyles() { const id = useId(); const menu = useFocusMenu(); return ( <div {...stylex.props(styles.customField)}> <ComboBox {...menu.root} items={frameworks}> <ComboBox.Label htmlFor={id} xstyle={styles.customLabel}> Framework </ComboBox.Label> <ComboBox.InputGroup xstyle={styles.customGroup}> <ComboBox.Input {...menu.input} id={id} placeholder="Search..." /> <ComboBox.Trigger aria-label="Show frameworks" xstyle={styles.muted}> <ComboBox.Indicator /> </ComboBox.Trigger> </ComboBox.InputGroup> <ComboBox.Portal> <ComboBox.Positioner> <ComboBox.Popover xstyle={styles.customPopover}> <ComboBox.List> {(item: (typeof frameworks)[number]) => ( <ComboBox.Item key={item.value} value={item} xstyle={styles.customItem}> {item.label} <ComboBox.ItemIndicator /> </ComboBox.Item> )} </ComboBox.List> </ComboBox.Popover> </ComboBox.Positioner> </ComboBox.Portal> </ComboBox> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
全局 CSS
可使用 @layer components 指令自定义 ComboBox 组件类。
了解更多。
@layer components { .combo-box { @apply flex flex-col gap-1; }
.combo-box__input-group { @apply relative inline-flex items-center; }
.combo-box__trigger { @apply absolute right-0 text-muted; }
.combo-box__popover { @apply rounded-lg border border-border bg-surface p-2; }}样式参考
HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。
CSS 类
ComboBox 组件使用以下 CSS 类(查看源码样式):
基础类 [!toc]
.combo-box- ComboBox 根容器.combo-box__input-group- 输入框与触发按钮的容器.combo-box__value- 已选值的展示区域(用于多选).combo-box__trigger- 打开 Popover 的按钮.combo-box__popover- Popover 容器
状态类 [!toc]
.combo-box[data-invalid="true"]- 无效状态.combo-box[data-disabled="true"]- 禁用状态.combo-box__trigger[data-focus-visible="true"]- 触发器聚焦.combo-box__trigger[data-disabled="true"]- 触发器禁用.combo-box__trigger[data-open="true"]- 展开状态
交互状态
组件同时支持 CSS 伪类与 data 属性:
- Hover:触发器上
:hover或[data-hovered="true"] - Focus:触发器上
:focus-visible或[data-focus-visible="true"] - Disabled:ComboBox 上
:disabled或[data-disabled="true"] - Open:触发器上
[data-open="true"]
API 参考
ComboBox
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
inputValue | string | - | 当前输入值(受控) |
defaultInputValue | string | - | 默认输入值(非受控) |
onInputChange | (value: string) => void | - | 输入值变化时的回调 |
selectionMode | "single" | "multiple" | "single" | 启用单选还是多选 |
selectedKey | Key | null | - | 当前选中的 key(受控,单选) |
defaultSelectedKey | Key | null | - | 默认选中的 key(非受控,单选) |
onSelectionChange | (key: Key | null) => void | - | 选中变化时的回调(单选) |
value | Key | null | Key[] | - | 当前选中的 key(受控)。当 selectionMode="multiple" 时为 Key[] |
defaultValue | Key | null | Key[] | - | 初始选中的 key(非受控)。当 selectionMode="multiple" 时为 Key[] |
onChange | (value: Key | null | Key[]) => void | - | 选中变化时的回调 |
items | Iterable<T> | - | 在 ListBox 中展示的 items |
disabledKeys | Iterable<Key> | - | 禁用项的 key |
defaultFilter | (text: string, inputValue: string) => boolean | - | 用于过滤 items 的自定义过滤函数 |
isDisabled | boolean | - | 是否禁用 ComboBox |
isReadOnly | boolean | - | 输入是否可选中但不可由用户更改 |
isRequired | boolean | - | 是否必填 |
isInvalid | boolean | - | ComboBox 的值是否无效 |
validate | (value: ComboBoxValidationValue) => ValidationError | true | null | undefined | - | 若给定值无效则返回错误信息的函数。当 validationBehavior="native" 时,提交表单会向用户展示校验错误;实时校验请改用 isInvalid prop |
validationBehavior | "native" | "aria" | "native" | 使用原生 HTML 表单校验在值缺失或无效时阻止提交,还是通过 ARIA 将字段标记为必填或无效 |
name | string | - | 提交 HTML 表单时 input 的 name |
form | string | - | 要关联的 <form> 元素 id |
formValue | "text" | "key" | "key" | 在 HTML 表单提交时提交选中项的文本还是 key。当 allowsCustomValue 为 true 时该选项不适用,始终提交文本 |
autoComplete | string | - | 自动完成行为类型 |
autoFocus | boolean | - | 是否在挂载时自动聚焦 |
allowsCustomValue | boolean | - | 是否允许不在列表中的自定义值 |
allowsEmptyCollection | boolean | - | 是否允许空集合 |
menuTrigger | "focus" | "input" | "manual" | "focus" | 展示 ComboBox 菜单所需的交互 |
shouldFocusWrap | boolean | - | 键盘导航是否循环 |
fullWidth | boolean | false | ComboBox 是否占满容器宽度 |
className | string | - | 附加 CSS 类 |
children | ReactNode | RenderFunction | - | ComboBox 内容或 render 函数 |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, ComboBoxRenderProps> | - | 使用自定义 render 函数覆盖默认 DOM 元素 |
ComboBox.InputGroup
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 附加 CSS 类 |
children | ReactNode | - | InputGroup 内容 |
ComboBox.Value
渲染 ComboBox 已选中的值,若未选中任何值则渲染占位符。默认情况下,已选项以逗号分隔的列表形式渲染。可使用 render 函数自定义(例如以标签形式展示)。
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
placeholder | ReactNode | - | 未选中任何项时展示的值 |
className | string | (values: ComboBoxValueRenderProps) => string | - | 附加 CSS 类 |
children | ReactNode | (values: ComboBoxValueRenderProps) => ReactNode | - | 自定义已选值的 render 函数 |
ComboBox.Trigger
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 附加 CSS 类 |
children | ReactNode | - | 自定义触发器内容 |
ComboBox.Popover
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
placement | "bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom" | "bottom" | 相对于触发器的 Popover 位置 |
className | string | - | 附加 CSS 类 |
children | ReactNode | - | 子内容 |
Render Props
对 ComboBox 使用 render 函数时,会传入以下值:
| Prop | 类型 | 描述 |
|---|---|---|
state | ComboBoxState | ComboBox 状态 |
inputValue | string | 当前输入值 |
selectedKey | Key | null | 当前选中的 key |
selectedItem | Node | null | 当前选中的 item |
示例
基本用法
import { ComboBox, Input, Label, ListBox } from '@lenso/ui';
<ComboBox className="w-[256px]"> <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover></ComboBox>代码示例:分组选项
import { ComboBox, Input, Label, ListBox, Header, Separator } from '@lenso/ui';
<ComboBox className="w-[256px]"> <Label>Country</Label> <ComboBox.InputGroup> <Input placeholder="Search countries..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Section> <Header>North America</Header> <ListBox.Item id="usa" textValue="United States"> United States <ListBox.ItemIndicator /> </ListBox.Item> </ListBox.Section> <Separator /> <ListBox.Section> <Header>Europe</Header> <ListBox.Item id="uk" textValue="United Kingdom"> United Kingdom <ListBox.ItemIndicator /> </ListBox.Item> </ListBox.Section> </ListBox> </ComboBox.Popover></ComboBox>受控选择
import type { Key } from '@lenso/ui';
import { ComboBox, Input, Label, ListBox } from '@lenso/ui';import { useState } from 'react';
function ControlledComboBox() { const [selectedKey, setSelectedKey] = useState<Key | null>('cat');
return ( <ComboBox className="w-[256px]" selectedKey={selectedKey} onSelectionChange={setSelectedKey} > <Label>Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> </ComboBox> );}代码示例:受控输入值
import { ComboBox, Input, Label, ListBox } from '@lenso/ui';import { useState } from 'react';
function ControlledInputComboBox() { const [inputValue, setInputValue] = useState('');
return ( <ComboBox className="w-[256px]" inputValue={inputValue} onInputChange={setInputValue} > <Label>Search</Label> <ComboBox.InputGroup> <Input placeholder="Type to search..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> </ComboBox> );}代码示例:异步加载
import { Collection, ComboBox, EmptyState, Input, Label, ListBox, ListBoxLoadMoreItem, Spinner } from '@lenso/ui';import { useAsyncList } from '@react-stately/data';
interface Character { name: string;}
function AsyncComboBox() { const list = useAsyncList<Character>({ async load({cursor, filterText, signal}) { const res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); const json = await res.json();
return { items: json.results, cursor: json.next, }; }, });
return ( <ComboBox allowsEmptyCollection className="w-[256px]" inputValue={list.filterText} onInputChange={list.setFilterText} > <Label>Pick a Character</Label> <ComboBox.InputGroup> <Input placeholder="Star Wars characters..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox renderEmptyState={() => <EmptyState />}> <Collection items={list.items}> {(item) => ( <ListBox.Item id={item.name} textValue={item.name}> {item.name} <ListBox.ItemIndicator /> </ListBox.Item> )} </Collection> <ListBoxLoadMoreItem isLoading={list.loadingState === "loadingMore"} onLoadMore={list.loadMore} > <div className="flex items-center justify-center gap-2 py-2"> <Spinner size="sm" /> <span className="text-sm text-muted">Loading more...</span> </div> </ListBoxLoadMoreItem> </ListBox> </ComboBox.Popover> </ComboBox> );}代码示例:自定义过滤
import { ComboBox, Input, Label, ListBox } from '@lenso/ui';
<ComboBox className="w-[256px]" defaultFilter={(text, inputValue) => { if (!inputValue) return true; return text.toLowerCase().includes(inputValue.toLowerCase()); }}> <Label>Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover></ComboBox>代码示例:菜单触发方式
使用 menuTrigger prop 控制 Popover 何时打开:
import { ComboBox, Description, Input, Label, ListBox } from '@lenso/ui';
// 在聚焦时打开(默认)<ComboBox className="w-[256px]" menuTrigger="focus"> <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <Description>Popover opens when the input is focused</Description></ComboBox>
// 在输入时打开<ComboBox className="w-[256px]" menuTrigger="input"> <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <Description>Popover opens when the user edits the input text</Description></ComboBox>
// 仅手动打开<ComboBox className="w-[256px]" menuTrigger="manual"> <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <Description>Popover only opens when the trigger button is pressed or arrow keys are used</Description></ComboBox>表单值
使用 formValue prop 控制提交表单时提交选中项的 key 还是文本:
import { Button, ComboBox, FieldError, Form, Input, Label, ListBox } from '@lenso/ui';
function FormValueExample() { const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); console.log('Submitted value:', formData.get('animal')); // Will be "cat" (the key) };
return ( <Form onSubmit={onSubmit}> {/* Submits the key (default) */} <ComboBox name="animal" formValue="key" isRequired> <Label>Animal</Label> <ComboBox.InputGroup> <Input placeholder="Select an animal..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <FieldError /> </ComboBox>
{/* Submits the text */} <ComboBox name="animal-text" formValue="text" isRequired> <Label>Animal (text)</Label> <ComboBox.InputGroup> <Input placeholder="Select an animal..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <FieldError /> </ComboBox>
<Button type="submit">Submit</Button> </Form> );}校验行为
使用 validationBehavior prop 控制校验信息的展示方式:
import { Button, ComboBox, FieldError, Form, Input, Label, ListBox } from '@lenso/ui';
function ValidationExample() { return ( <div className="space-y-8"> {/* Native validation (default) - blocks form submission */} <Form> <ComboBox name="animal" isRequired validationBehavior="native"> <Label>Animal (native validation)</Label> <ComboBox.InputGroup> <Input placeholder="Select an animal..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <FieldError /> </ComboBox> <Button type="submit">Submit</Button> </Form>
{/* ARIA validation - shows errors in realtime, doesn't block submission */} <Form> <ComboBox name="animal-aria" isRequired validationBehavior="aria"> <Label>Animal (ARIA validation)</Label> <ComboBox.InputGroup> <Input placeholder="Select an animal..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <FieldError /> </ComboBox> <Button type="submit">Submit</Button> </Form> </div> );}自定义校验
使用 validate prop 添加自定义校验逻辑:
import { ComboBox, FieldError, Input, Label, ListBox } from '@lenso/ui';
function CustomValidationExample() { return ( <ComboBox className="w-[256px]" isRequired validate={(value) => { if (!value || value.selectedKey === null) { return 'Please select an animal'; } if (value.selectedKey === 'snake') { return 'Snakes are not allowed'; } return true; }} > <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="snake" textValue="Snake"> Snake <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover> <FieldError /> </ComboBox> );}只读
使用 isReadOnly 将 ComboBox 设为只读:
import { ComboBox, Input, Label, ListBox } from '@lenso/ui';
<ComboBox className="w-[256px]" isReadOnly defaultSelectedKey="cat"> <Label>Favorite Animal</Label> <ComboBox.InputGroup> <Input placeholder="Search animals..." /> <ComboBox.Trigger /> </ComboBox.InputGroup> <ComboBox.Popover> <ListBox> <ListBox.Item id="cat" textValue="Cat"> Cat <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Item id="dog" textValue="Dog"> Dog <ListBox.ItemIndicator /> </ListBox.Item> </ListBox> </ComboBox.Popover></ComboBox>无障碍
ComboBox 实现 ARIA ComboBox 模式,并提供:
- 完整键盘导航
- 选择与输入变化时的屏幕阅读器播报
- 合理的焦点管理
- 禁用状态支持
- 输入过滤(typeahead)式搜索
- 与 HTML 表单的集成
- 自定义值支持
更多信息见 React Aria ComboBox 文档。