Skip to content
Lenso UI

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(默认):输入框获得焦点时打开 Popover
  • input:用户编辑输入文本时打开 Popover
  • manual:仅当用户按下触发按钮或使用方向键时打开 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类型默认值描述
inputValuestring-当前输入值(受控)
defaultInputValuestring-默认输入值(非受控)
onInputChange(value: string) => void-输入值变化时的回调
selectionMode"single" | "multiple""single"启用单选还是多选
selectedKeyKey | null-当前选中的 key(受控,单选)
defaultSelectedKeyKey | null-默认选中的 key(非受控,单选)
onSelectionChange(key: Key | null) => void-选中变化时的回调(单选)
valueKey | null | Key[]-当前选中的 key(受控)。当 selectionMode="multiple" 时为 Key[]
defaultValueKey | null | Key[]-初始选中的 key(非受控)。当 selectionMode="multiple" 时为 Key[]
onChange(value: Key | null | Key[]) => void-选中变化时的回调
itemsIterable<T>-在 ListBox 中展示的 items
disabledKeysIterable<Key>-禁用项的 key
defaultFilter(text: string, inputValue: string) => boolean-用于过滤 items 的自定义过滤函数
isDisabledboolean-是否禁用 ComboBox
isReadOnlyboolean-输入是否可选中但不可由用户更改
isRequiredboolean-是否必填
isInvalidboolean-ComboBox 的值是否无效
validate(value: ComboBoxValidationValue) => ValidationError | true | null | undefined-若给定值无效则返回错误信息的函数。当 validationBehavior="native" 时,提交表单会向用户展示校验错误;实时校验请改用 isInvalid prop
validationBehavior"native" | "aria""native"使用原生 HTML 表单校验在值缺失或无效时阻止提交,还是通过 ARIA 将字段标记为必填或无效
namestring-提交 HTML 表单时 input 的 name
formstring-要关联的 <form> 元素 id
formValue"text" | "key""key"在 HTML 表单提交时提交选中项的文本还是 key。当 allowsCustomValue 为 true 时该选项不适用,始终提交文本
autoCompletestring-自动完成行为类型
autoFocusboolean-是否在挂载时自动聚焦
allowsCustomValueboolean-是否允许不在列表中的自定义值
allowsEmptyCollectionboolean-是否允许空集合
menuTrigger"focus" | "input" | "manual""focus"展示 ComboBox 菜单所需的交互
shouldFocusWrapboolean-键盘导航是否循环
fullWidthbooleanfalseComboBox 是否占满容器宽度
classNamestring-附加 CSS 类
childrenReactNode | RenderFunction-ComboBox 内容或 render 函数
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, ComboBoxRenderProps>-使用自定义 render 函数覆盖默认 DOM 元素

ComboBox.InputGroup

Prop类型默认值描述
classNamestring-附加 CSS 类
childrenReactNode-InputGroup 内容

ComboBox.Value

渲染 ComboBox 已选中的值,若未选中任何值则渲染占位符。默认情况下,已选项以逗号分隔的列表形式渲染。可使用 render 函数自定义(例如以标签形式展示)。

Prop类型默认值描述
placeholderReactNode-未选中任何项时展示的值
classNamestring | (values: ComboBoxValueRenderProps) => string-附加 CSS 类
childrenReactNode | (values: ComboBoxValueRenderProps) => ReactNode-自定义已选值的 render 函数

ComboBox.Trigger

Prop类型默认值描述
classNamestring-附加 CSS 类
childrenReactNode-自定义触发器内容

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 位置
classNamestring-附加 CSS 类
childrenReactNode-子内容

Render Props

对 ComboBox 使用 render 函数时,会传入以下值:

Prop类型描述
stateComboBoxStateComboBox 状态
inputValuestring当前输入值
selectedKeyKey | null当前选中的 key
selectedItemNode | 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 文档。

相关组件