"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
用法
¥Usage
import { Combobox } from "@chakra-ui/react"
<Combobox.Root>
<Combobox.Label />
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty />
<Combobox.Item />
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel />
<Combobox.Item />
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
要设置组合框,你可能需要导入以下钩子:
¥To setup combobox, you might need to import the following hooks:
-
useListCollection:用于管理组合框中的项目列表,并提供用于过滤和修改列表的实用方法。 -
useFilter:用于基于Intl.CollatorAPI 为组合框提供过滤逻辑。
示例
¥Examples
基础
¥Basic
基本组合框提供了一个可搜索的单选下拉菜单。
¥The basic combobox provides a searchable dropdown with single selection.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
尺寸
¥Sizes
将 size 属性传递给 Combobox.Root,以更改组合框的大小。
¥Pass the size prop to the Combobox.Root to change the size of the combobox.
"use client"
import {
Combobox,
Portal,
Stack,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
return (
<Stack gap="8">
<ComboboxDemo size="xs" />
<ComboboxDemo size="sm" />
<ComboboxDemo size="md" />
<ComboboxDemo size="lg" />
</Stack>
)
}
const ComboboxDemo = (props: Omit<Combobox.RootProps, "collection">) => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
{...props}
onInputValueChange={(e) => filter(e.inputValue)}
collection={collection}
>
<Combobox.Label>
Select framework ({props.size?.toString()})
</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
变量
¥Variants
将 variant 属性传递给 Combobox.Root,以更改组合框的外观。
¥Pass the variant prop to the Combobox.Root to change the appearance of the
combobox.
"use client"
import {
Combobox,
Portal,
Stack,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
return (
<Stack gap="8">
<ComboboxDemo variant="subtle" />
<ComboboxDemo variant="outline" />
<ComboboxDemo variant="flushed" />
</Stack>
)
}
const ComboboxDemo = (props: Omit<Combobox.RootProps, "collection">) => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
{...props}
onInputValueChange={(e) => filter(e.inputValue)}
collection={collection}
>
<Combobox.Label>
Select framework ({props.variant?.toString()})
</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
多个
¥Multiple
将 multiple 属性传递给 Combobox.Root 以启用多选。这允许用户从列表中选择多个项目。
¥Pass the multiple prop to the Combobox.Root to enable multiple selection.
This allows users to select multiple items from the list.
设置此值后,组合框在选择项目时将始终清除输入值。
"use client"
import {
Badge,
Combobox,
Portal,
Wrap,
createListCollection,
} from "@chakra-ui/react"
import { useMemo, useState } from "react"
const skills = [
"JavaScript",
"TypeScript",
"React",
"Node.js",
"GraphQL",
"PostgreSQL",
]
const Demo = () => {
const [searchValue, setSearchValue] = useState("")
const [selectedSkills, setSelectedSkills] = useState<string[]>([])
const filteredItems = useMemo(
() =>
skills.filter((item) =>
item.toLowerCase().includes(searchValue.toLowerCase()),
),
[searchValue],
)
const collection = useMemo(
() => createListCollection({ items: filteredItems }),
[filteredItems],
)
const handleValueChange = (details: Combobox.ValueChangeDetails) => {
setSelectedSkills(details.value)
}
return (
<Combobox.Root
multiple
closeOnSelect
width="320px"
value={selectedSkills}
collection={collection}
onValueChange={handleValueChange}
onInputValueChange={(details) => setSearchValue(details.inputValue)}
>
<Wrap gap="2">
{selectedSkills.map((skill) => (
<Badge key={skill}>{skill}</Badge>
))}
</Wrap>
<Combobox.Label>Select Skills</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Skills</Combobox.ItemGroupLabel>
{filteredItems.map((item) => (
<Combobox.Item key={item} item={item}>
{item}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
<Combobox.Empty>No skills found</Combobox.Empty>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
异步加载
¥Async Loading
以下是如何在用户输入时异步加载 collection 的示例,非常适合 API 驱动的搜索界面。
¥Here's an example of loading the collection asynchronously as users type,
perfect for API-driven search interfaces.
"use client"
import {
Combobox,
HStack,
Portal,
Span,
Spinner,
useListCollection,
} from "@chakra-ui/react"
import { useState } from "react"
import { useAsync } from "react-use"
const Demo = () => {
const [inputValue, setInputValue] = useState("")
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
const state = useAsync(async () => {
const response = await fetch(
`https://swapi.py4e.com/api/people/?search=${inputValue}`,
)
const data = await response.json()
set(data.results)
}, [inputValue, set])
return (
<Combobox.Root
width="320px"
collection={collection}
placeholder="Example: C-3PO"
onInputValueChange={(e) => setInputValue(e.inputValue)}
positioning={{ sameWidth: false, placement: "bottom-start" }}
>
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content minW="sm">
{state.loading ? (
<HStack p="2">
<Spinner size="xs" borderWidth="1px" />
<Span>Loading...</Span>
</HStack>
) : state.error ? (
<Span p="2" color="fg.error">
Error fetching
</Span>
) : (
collection.items?.map((character) => (
<Combobox.Item key={character.name} item={character}>
<HStack justify="space-between" textStyle="sm">
<Span fontWeight="medium" truncate>
{character.name}
</Span>
<Span color="fg.muted" truncate>
{character.height}cm / {character.mass}kg
</Span>
</HStack>
<Combobox.ItemIndicator />
</Combobox.Item>
))
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
高亮匹配文本
¥Highlight Matching Text
以下是组合 Combobox.Item 和 Highlight 组件以在搜索结果中高亮匹配文本的示例。
¥Here's an example of composing the Combobox.Item and Highlight components to
highlight matching text in search results.
"use client"
import {
Combobox,
Highlight,
Portal,
useComboboxContext,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<ComboboxItem item={item} key={item.value} />
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
function ComboboxItem(props: { item: { label: string; value: string } }) {
const { item } = props
const combobox = useComboboxContext()
return (
<Combobox.Item item={item} key={item.value}>
<Combobox.ItemText>
<Highlight
ignoreCase
query={combobox.inputValue}
styles={{ bg: "yellow.emphasized", fontWeight: "medium" }}
>
{item.label}
</Highlight>
</Combobox.ItemText>
</Combobox.Item>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
点击打开
¥Open on Click
使用 openOnClick 属性在用户点击输入框时打开组合框。
¥Use the openOnClick prop to open the combobox when the user clicks on the
input.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
openOnClick
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
自定义对象
¥Custom Objects
默认情况下,组合框集合需要一个包含 label 和 value 属性的对象数组。在某些情况下,你可能需要处理自定义对象。
¥By default, the combobox collection expects an array of objects with label and
value properties. In some cases, you may need to deal with custom objects.
使用 itemToString 和 itemToValue 属性将自定义对象映射到所需的接口。
¥Use the itemToString and itemToValue props to map the custom object to the
required interface.
const items = [
{ country: "United States", code: "US", flag: "🇺🇸" },
{ country: "Canada", code: "CA", flag: "🇨🇦" },
{ country: "Australia", code: "AU", flag: "🇦🇺" },
// ...
]
const { contains } = useFilter({ sensitivity: "base" })
const { collection } = useListCollection({
initialItems: items,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: contains,
})
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: countries,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
collection={collection}
onInputValueChange={handleInputChange}
>
<Combobox.Label>Search Countries</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="e.g. United States" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item key={item.code} item={item}>
{item.country}
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const countries = [
{ country: "United States", code: "US", flag: "🇺🇸" },
{ country: "Canada", code: "CA", flag: "🇨🇦" },
{ country: "Australia", code: "AU", flag: "🇦🇺" },
{ country: "United Kingdom", code: "UK", flag: "🇬🇧" },
{ country: "New Zealand", code: "NZ", flag: "🇳🇿" },
{ country: "South Africa", code: "ZA", flag: "🇿🇦" },
{ country: "India", code: "IN", flag: "🇮🇳" },
{ country: "China", code: "CN", flag: "🇨🇳" },
{ country: "Japan", code: "JP", flag: "🇯🇵" },
{ country: "Korea", code: "KR", flag: "🇰🇷" },
{ country: "Vietnam", code: "VN", flag: "🇻🇳" },
{ country: "Thailand", code: "TH", flag: "🇹🇭" },
{ country: "Malaysia", code: "MY", flag: "🇲🇾" },
{ country: "Indonesia", code: "ID", flag: "🇮🇩" },
{ country: "Philippines", code: "PH", flag: "🇵🇭" },
{ country: "Singapore", code: "SG", flag: "🇸🇬" },
{ country: "Hong Kong", code: "HK", flag: "🇭🇰" },
{ country: "Macau", code: "MO", flag: "🇲🇴" },
{ country: "Taiwan", code: "TW", flag: "🇹🇼" },
]
最小字符数
¥Minimum Characters
使用 openOnChange 属性设置过滤列表前的最小字符数。
¥Use the openOnChange prop to set a minimum number of characters before
filtering the list.
<Combobox.Root openOnChange={(e) => e.inputValue.length > 2} />
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
openOnChange={(e) => e.inputValue.length > 2}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
字段
¥Field
组合 Combobox 组件与 Field 组件,使其将组合框封装在表单字段中。用于表单布局。
¥Compose the Combobox component with the Field component to wrap the combobox
in a form field. Useful for form layouts.
"use client"
import {
Combobox,
Field,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Field.Root width="320px">
<Field.Label>Select framework</Field.Label>
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Field.HelperText>The framework you love to use</Field.HelperText>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
</Field.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
表单 + 自定义对象
¥Form + Custom Object
在表单中使用自定义对象时,你通常需要提交编程值而不是显示值。本示例展示了如何使用隐藏输入框将自定义对象映射与表单提交相结合。
¥When working with custom objects in forms, you often need to submit the programmatic value rather than the display value. This example shows how to combine custom object mapping with form submission using a hidden input.
关键在于使用 itemToValue 定义提交的内容,而 itemToString 控制用户看到的内容。隐藏输入框用于捕获表单提交的程序化值。
¥The key is using itemToValue to define what gets submitted, while
itemToString controls what users see. A hidden input captures the programmatic
value for form submission.
在此示例中,用户看到 "🇺🇸 美国",但表单提交的是 "US"。
"use client"
import {
Button,
Combobox,
Field,
Portal,
Stack,
useComboboxContext,
useFilter,
useListCollection,
} from "@chakra-ui/react"
// This is a hidden input that is used to store the value of the combobox
const ComboboxHiddenInput = (props: React.ComponentProps<"input">) => {
const combobox = useComboboxContext()
return <input type="hidden" value={combobox.value[0]} readOnly {...props} />
}
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: countries,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
filter: contains,
})
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const country = formData.get("country")
console.log("Form submitted with country code:", country)
alert(`Selected country code: ${country}`)
}
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<form onSubmit={handleSubmit}>
<Stack gap="4" align="flex-start">
<Field.Root width="320px">
<Field.Label>Country</Field.Label>
<Combobox.Root
collection={collection}
onInputValueChange={handleInputChange}
>
<Combobox.Control>
<Combobox.Input placeholder="Search countries (e.g. United States)" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<ComboboxHiddenInput name="country" />
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No countries found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item key={item.code} item={item}>
{item.flag} {item.country}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
<Field.HelperText>
The form will submit the country code (e.g. "US"), not the display
name
</Field.HelperText>
</Field.Root>
<Button size="sm" type="submit">
Submit
</Button>
</Stack>
</form>
)
}
const countries = [
{ country: "United States", code: "US", flag: "🇺🇸" },
{ country: "Canada", code: "CA", flag: "🇨🇦" },
{ country: "Australia", code: "AU", flag: "🇦🇺" },
{ country: "United Kingdom", code: "GB", flag: "🇬🇧" },
{ country: "New Zealand", code: "NZ", flag: "🇳🇿" },
{ country: "South Africa", code: "ZA", flag: "🇿🇦" },
{ country: "India", code: "IN", flag: "🇮🇳" },
{ country: "China", code: "CN", flag: "🇨🇳" },
{ country: "Japan", code: "JP", flag: "🇯🇵" },
{ country: "Korea", code: "KR", flag: "🇰🇷" },
{ country: "Vietnam", code: "VN", flag: "🇻🇳" },
{ country: "Thailand", code: "TH", flag: "🇹🇭" },
{ country: "Malaysia", code: "MY", flag: "🇲🇾" },
{ country: "Indonesia", code: "ID", flag: "🇮🇩" },
{ country: "Philippines", code: "PH", flag: "🇵🇭" },
{ country: "Singapore", code: "SG", flag: "🇸🇬" },
{ country: "Hong Kong", code: "HK", flag: "🇭🇰" },
{ country: "Macau", code: "MO", flag: "🇲🇴" },
{ country: "Taiwan", code: "TW", flag: "🇹🇼" },
]
钩子表单
¥Hook Form
本示例演示如何使用 Controller 组件将 Combobox 与 React Hook Form 集成。表单会自动接收项目的 value 属性,无需隐藏输入框。
¥This example demonstrates how to integrate the Combobox with React Hook Form
using the Controller component. The form automatically receives the item's
value property without needing a hidden input.
用户看到 "React",但表单收到的是 "react"。
¥Users see "React" but the form receives "react".
"use client"
import {
Button,
Combobox,
Field,
Portal,
Stack,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
const formSchema = z.object({
framework: z.string({ message: "Framework is required" }).min(1),
})
type FormValues = z.infer<typeof formSchema>
const Demo = () => {
const {
handleSubmit,
formState: { errors },
control,
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
})
const onSubmit = handleSubmit((data) => {
console.log("Form submitted with:", data)
alert(`Selected framework: ${data.framework}`)
})
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<form onSubmit={onSubmit}>
<Stack gap="4" align="flex-start">
<Field.Root invalid={!!errors.framework} width="320px">
<Field.Label>Framework</Field.Label>
<Controller
control={control}
name="framework"
render={({ field }) => (
<Combobox.Root
collection={collection}
value={field.value ? [field.value] : []}
onValueChange={({ value }) => field.onChange(value[0] || "")}
onInputValueChange={handleInputChange}
onInteractOutside={() => field.onBlur()}
>
<Combobox.Control>
<Combobox.Input placeholder="Select framework" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No frameworks found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item key={item.value} item={item}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)}
/>
<Field.ErrorText>{errors.framework?.message}</Field.ErrorText>
</Field.Root>
<Button size="sm" type="submit">
Submit
</Button>
</Stack>
</form>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Solid", value: "solid" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine", value: "alpine" },
]
已禁用状态
¥Disabled State
将 disabled 属性传递给 Combobox.Root 以禁用整个组合框。
¥Pass the disabled prop to the Combobox.Root to disable the entire combobox.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
disabled
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
已禁用项目
¥Disabled Item
要禁用下拉菜单中的特定项目,请将 disabled 属性添加到集合项目。
¥Disable specific items in the dropdown, add the disabled prop to the
collection item.
const items = [
{ label: "Item 1", value: "item-1", disabled: true },
{ label: "Item 2", value: "item-2" },
]
const { collection } = useListCollection({
initialItems: items,
// ...
})
"use client"
import {
Combobox,
HStack,
Icon,
Portal,
Span,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: companies,
filter: contains,
itemToValue: (item) => item.id,
itemToString: (item) => item.name,
isItemDisabled: (item) => !!item.disabled,
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
filter(details.inputValue)
}
return (
<Combobox.Root
width="320px"
collection={collection}
placeholder="Type to search companies"
onInputValueChange={handleInputChange}
>
<Combobox.Label>Select a Company</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Companies</Combobox.ItemGroupLabel>
{collection.items.map((country) => {
return (
<Combobox.Item item={country} key={country.id}>
<HStack gap="3">
<Icon>{country.logo}</Icon>
<Span fontWeight="medium">{country.name}</Span>
</HStack>
<Combobox.ItemIndicator />
</Combobox.Item>
)
})}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
interface Company {
id: string
name: string
logo: React.ReactElement
disabled?: boolean
}
const companies: Company[] = [
{
id: "airbnb",
name: "Airbnb",
logo: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<g clipPath="url(#airbnb)">
<path fill="#EB4C60" d="M0 0h18v18H0V0Z" />
<path
fill="#fff"
d="m13.565 10.777.051.123c.133.372.173.724.092 1.076a2.142 2.142 0 0 1-1.33 1.672 2.095 2.095 0 0 1-1.096.141 2.737 2.737 0 0 1-1.023-.342c-.41-.231-.819-.564-1.269-1.047-.45.483-.85.816-1.27 1.047a2.73 2.73 0 0 1-1.29.362c-.286 0-.562-.05-.828-.16a2.146 2.146 0 0 1-1.33-1.673 2.211 2.211 0 0 1 .122-1.087c.051-.13.103-.252.153-.362l.112-.242.124-.271.011-.02a115.31 115.31 0 0 1 2.261-4.552l.03-.061c.083-.151.165-.312.246-.473a3.45 3.45 0 0 1 .37-.553 1.725 1.725 0 0 1 1.31-.605c.501 0 .972.221 1.299.625.15.167.25.342.344.51l.025.043c.081.161.163.322.246.473l.03.061a104.224 104.224 0 0 1 2.262 4.552l.01.01.124.271.112.242c.034.073.067.156.102.24Zm-5.6-1.227c.123.544.482 1.188 1.035 1.873.552-.695.911-1.339 1.034-1.873.05-.201.06-.41.03-.615a.968.968 0 0 0-.163-.422C9.715 8.232 9.379 8.07 9 8.07a1.092 1.092 0 0 0-.9.443.968.968 0 0 0-.165.423c-.03.205-.019.414.031.615l-.001-.001Zm4.187 3.524c.503-.201.86-.654.932-1.178.037-.26.013-.526-.071-.775a1.97 1.97 0 0 0-.088-.216 5.032 5.032 0 0 1-.046-.107 7.415 7.415 0 0 1-.118-.251 5.735 5.735 0 0 0-.117-.252v-.01a132.7 132.7 0 0 0-2.242-4.53l-.03-.061-.123-.232-.123-.232a2.211 2.211 0 0 0-.287-.443 1.078 1.078 0 0 0-.819-.372 1.078 1.078 0 0 0-.818.372c-.113.136-.21.284-.287.443-.042.077-.083.155-.123.232-.04.079-.082.157-.123.232l-.03.06a109.354 109.354 0 0 0-2.253 4.521l-.01.02a20.74 20.74 0 0 0-.281.61 1.951 1.951 0 0 0-.087.216 1.639 1.639 0 0 0-.092.785 1.5 1.5 0 0 0 .931 1.178c.235.09.502.13.778.1.257-.03.512-.11.778-.26.369-.202.748-.515 1.167-.978-.665-.816-1.084-1.57-1.239-2.235a2.058 2.058 0 0 1-.051-.855c.041-.253.134-.484.277-.685.317-.443.85-.716 1.442-.716.595 0 1.127.263 1.444.716.143.2.235.432.276.685.031.261.021.543-.051.855-.153.665-.563 1.41-1.239 2.225.43.464.8.776 1.167.977.266.15.522.231.778.262.267.03.533 0 .778-.101Z"
/>
</g>
<defs>
<clipPath id="airbnb">
<path fill="#fff" d="M0 0h18v18H0z" />
</clipPath>
</defs>
</svg>
),
},
{
id: "tesla",
disabled: true,
logo: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<g clipPath="url(#tesla)">
<path fill="#E31937" d="M0 0h18v18H0V0Z" />
<path
fill="#fff"
d="m9 15 1.5-8c1.334 0 1.654.272 1.715.872 0 0 .894-.335 1.346-1.016C11.8 6.037 10 6 10 6L9 7.25 8 6s-1.8.037-3.56.856c.45.68 1.345 1.016 1.345 1.016.061-.6.39-.871 1.715-.872L9 15Z"
/>
<path
fill="#fff"
d="M9 5.608a11.35 11.35 0 0 1 4.688.955C13.91 6.16 14 6 14 6c-1.823-.724-3.53-.994-5-1-1.47.006-3.177.276-5 1 0 0 .114.2.313.563A11.348 11.348 0 0 1 9 5.608Z"
/>
</g>
<defs>
<clipPath id="tesla">
<path fill="#fff" d="M0 0h18v18H0z" />
</clipPath>
</defs>
</svg>
),
name: "Tesla",
},
{
logo: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<g clipPath="url(#nvidia-a)">
<path fill="url(#nvidia-b)" d="M0 0h18v18H0V0Z" />
<path
fill="#fff"
d="M7.601 7.57v-.656c.065-.004.13-.008.195-.008 1.797-.057 2.975 1.547 2.975 1.547S9.5 10.218 8.136 10.218c-.183 0-.36-.029-.53-.085V8.14c.7.085.841.393 1.258 1.093l.936-.786s-.685-.894-1.834-.894a2.745 2.745 0 0 0-.365.016Zm0-2.17v.98l.195-.012c2.497-.086 4.13 2.048 4.13 2.048s-1.871 2.275-3.819 2.275c-.17 0-.336-.016-.502-.044v.607c.138.016.28.029.417.029 1.814 0 3.126-.928 4.397-2.02.21.17 1.073.578 1.251.756-1.206 1.012-4.02 1.826-5.615 1.826-.154 0-.3-.008-.446-.024v.854H14.5V5.4H7.601Zm0 4.733v.518c-1.676-.3-2.141-2.045-2.141-2.045s.805-.89 2.141-1.036v.567h-.004c-.7-.085-1.25.57-1.25.57s.31 1.106 1.254 1.426Zm-2.975-1.6s.991-1.465 2.98-1.619V6.38C5.402 6.558 3.5 8.42 3.5 8.42s1.077 3.118 4.101 3.401v-.567c-2.218-.275-2.975-2.72-2.975-2.72Z"
/>
</g>
<defs>
<linearGradient
id="nvidia-b"
x1="16"
x2="5.5"
y1="-.5"
y2="18"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#85B737" />
<stop offset="1" stopColor="#597B20" />
</linearGradient>
<clipPath id="nvidia-a">
<path fill="#fff" d="M0 0h18v18H0z" />
</clipPath>
</defs>
</svg>
),
id: "nvida",
name: "NVIDA",
},
{
id: "amazon",
name: "Amazon",
logo: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<g clipPath="url(#amazon)">
<path d="M0 0h18v18H0V0Z" />
<path
fill="#fff"
d="M12.237 10.734c-.259-.327-.458-.56-.458-1.189V7.46c0-.88-.06-1.703-.708-2.306-.519-.478-1.373-.654-2.047-.654-1.425 0-2.698.58-3.01 2.137-.026.177.104.252.207.278l1.351.123c.13 0 .208-.125.234-.25.104-.529.572-.972 1.09-.972.285 0 .848.287.848.89v.754c-.83 0-1.757.056-2.483.357-.855.353-1.586 1.028-1.586 2.11 0 1.382 1.064 2.137 2.204 2.137.96 0 1.482-.25 2.232-.979.235.352.38.603.82.979.105.051.234.051.31-.024.26-.228.712-.703.996-.929.13-.102.104-.252 0-.377ZM9.744 8.775c0 .502-.098 1.756-1.368 1.756-.653 0-.666-.769-.666-.769 0-.988 1.049-1.317 2.034-1.317v.33Z"
/>
<path
fill="#FFB300"
d="M12.917 12.952C11.862 13.601 10.284 14 9.005 14a7.818 7.818 0 0 1-4.713-1.551c-.101-.084 0-.168.1-.126 1.432.685 3 1.036 4.587 1.026 1.154 0 2.609-.209 3.787-.628.174-.042.325.126.15.231Zm.376-.44c-.125-.147-.878-.063-1.204-.043-.101 0-.125-.062-.025-.125.576-.357 1.554-.252 1.655-.126.1.126-.026.943-.577 1.32-.076.064-.176.021-.126-.04.126-.253.402-.84.276-.987Z"
/>
</g>
<defs>
<clipPath id="amazon">
<path fill="#fff" d="M0 0h18v18H0z" />
</clipPath>
</defs>
</svg>
),
},
]
输入框组
¥Input Group
与 InputGroup 组合以添加图标或其他元素。
¥Combine with InputGroup to add icons or other elements.
"use client"
import {
Combobox,
InputGroup,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { LuCode } from "react-icons/lu"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<InputGroup startElement={<LuCode />}>
<Combobox.Input placeholder="Type to search" />
</InputGroup>
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
无效
¥Invalid
将 invalid 属性传递给 Combobox.Root 以显示错误状态。
¥Pass the invalid prop to the Combobox.Root to show the error state.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
invalid
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
受控值
¥Controlled Value
使用 value 和 onValueChange 属性以编程方式控制组合框的值。
¥Use the value and onValueChange props to control the combobox's value
programmatically.
"use client"
import {
Badge,
Combobox,
For,
HStack,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { useState } from "react"
const Demo = () => {
const [value, setValue] = useState<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
value={value}
onValueChange={(e) => setValue(e.value)}
width="320px"
>
<HStack textStyle="sm" mb="6">
Selected:
<HStack>
<For each={value} fallback="N/A">
{(v) => <Badge key={v}>{v}</Badge>}
</For>
</HStack>
</HStack>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
商店
¥Store
控制组合框的另一种方法是使用 Combobox.RootProvider 组件和 useCombobox 存储钩子。
¥An alternative way to control the combobox is to use the Combobox.RootProvider
component and the useCombobox store hook.
import { Combobox, useCombobox } from "@chakra-ui/react"
function Demo() {
const combobox = useCombobox()
return (
<Combobox.RootProvider value={combobox}>{/* ... */}</Combobox.RootProvider>
)
}
这样,你就可以从组合框外部访问组合框的状态和方法。
¥This way you can access the combobox state and methods from outside the combobox.
"use client"
import {
Combobox,
Portal,
useCombobox,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
const combobox = useCombobox({
collection,
onInputValueChange(e) {
filter(e.inputValue)
},
})
return (
<Combobox.RootProvider value={combobox} width="320px">
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
受控打开
¥Controlled Open
使用 open 和 onOpenChange 属性以编程方式控制组合框的打开状态。
¥Use the open and onOpenChange props to control the combobox's open state
programmatically.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { useState } from "react"
const Demo = () => {
const [open, setOpen] = useState(false)
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
open={open}
onOpenChange={(e) => setOpen(e.open)}
>
<Combobox.Label>Combobox is {open ? "open" : "closed"}</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
限制大型数据集
¥Limit Large Datasets
管理大型列表的推荐方法是在 useListCollection 钩子上使用 limit 属性。这将限制 DOM 中渲染项的数量,以提高性能。
¥The recommended way of managing large lists is to use the limit property on
the useListCollection hook. This will limit the number of rendered items in
the DOM to improve performance.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { useRef } from "react"
const Demo = () => {
const contentRef = useRef<HTMLDivElement>(null)
const { startsWith } = useFilter({ sensitivity: "base" })
const { collection, filter, reset } = useListCollection({
initialItems: items,
filter: startsWith,
limit: 10,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
openOnClick
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger onClick={reset} />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content ref={contentRef}>
{collection.items.map((item) => (
<Combobox.Item key={item.value} item={item}>
<Combobox.ItemText truncate>
<span aria-hidden style={{ marginRight: 4 }}>
{item.emoji}
</span>
{item.label}
</Combobox.ItemText>
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
export const items = [
{ value: "AD", label: "Andorra", emoji: "🇦🇩" },
{ value: "AE", label: "United Arab Emirates", emoji: "🇦🇪" },
{ value: "AF", label: "Afghanistan", emoji: "🇦🇫" },
{ value: "AG", label: "Antigua and Barbuda", emoji: "🇦🇬" },
{ value: "AI", label: "Anguilla", emoji: "🇦🇮" },
{ value: "AL", label: "Albania", emoji: "🇦🇱" },
{ value: "AM", label: "Armenia", emoji: "🇦🇲" },
{ value: "AO", label: "Angola", emoji: "🇦🇴" },
{ value: "AQ", label: "Antarctica", emoji: "🇦🇶" },
{ value: "AR", label: "Argentina", emoji: "🇦🇷" },
{ value: "AS", label: "American Samoa", emoji: "🇦🇸" },
{ value: "AT", label: "Austria", emoji: "🇦🇹" },
{ value: "AU", label: "Australia", emoji: "🇦🇺" },
{ value: "AW", label: "Aruba", emoji: "🇦🇼" },
{ value: "AX", label: "Åland Islands", emoji: "🇦🇽" },
{ value: "AZ", label: "Azerbaijan", emoji: "🇦🇿" },
{ value: "BA", label: "Bosnia and Herzegovina", emoji: "🇧🇦" },
{ value: "BB", label: "Barbados", emoji: "🇧🇧" },
{ value: "BD", label: "Bangladesh", emoji: "🇧🇩" },
{ value: "BE", label: "Belgium", emoji: "🇧🇪" },
{ value: "BF", label: "Burkina Faso", emoji: "🇧🇫" },
{ value: "BG", label: "Bulgaria", emoji: "🇧🇬" },
{ value: "BH", label: "Bahrain", emoji: "🇧🇭" },
{ value: "BI", label: "Burundi", emoji: "🇧🇮" },
{ value: "BJ", label: "Benin", emoji: "🇧🇯" },
{ value: "BL", label: "Saint Barthélemy", emoji: "🇧🇱" },
{ value: "BM", label: "Bermuda", emoji: "🇧🇲" },
{ value: "BN", label: "Brunei Darussalam", emoji: "🇧🇳" },
{ value: "BO", label: "Bolivia, Plurinational State of", emoji: "🇧🇴" },
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba", emoji: "🇧🇶" },
{ value: "BR", label: "Brazil", emoji: "🇧🇷" },
{ value: "BS", label: "Bahamas", emoji: "🇧🇸" },
{ value: "BT", label: "Bhutan", emoji: "🇧🇹" },
{ value: "BV", label: "Bouvet Island", emoji: "🇧🇻" },
{ value: "BW", label: "Botswana", emoji: "🇧🇼" },
{ value: "BY", label: "Belarus", emoji: "🇧🇾" },
{ value: "BZ", label: "Belize", emoji: "🇧🇿" },
{ value: "CA", label: "Canada", emoji: "🇨🇦" },
{ value: "CC", label: "Cocos (Keeling) Islands", emoji: "🇨🇨" },
{ value: "CD", label: "Congo, Democratic Republic of the", emoji: "🇨🇩" },
{ value: "CF", label: "Central African Republic", emoji: "🇨🇫" },
{ value: "CG", label: "Congo", emoji: "🇨🇬" },
{ value: "CH", label: "Switzerland", emoji: "🇨🇭" },
{ value: "CI", label: "Côte d'Ivoire", emoji: "🇨🇮" },
{ value: "CK", label: "Cook Islands", emoji: "🇨🇰" },
{ value: "CL", label: "Chile", emoji: "🇨🇱" },
{ value: "CM", label: "Cameroon", emoji: "🇨🇲" },
{ value: "CN", label: "China", emoji: "🇨🇳" },
{ value: "CO", label: "Colombia", emoji: "🇨🇴" },
{ value: "CR", label: "Costa Rica", emoji: "🇨🇷" },
{ value: "CU", label: "Cuba", emoji: "🇨🇺" },
{ value: "CV", label: "Cabo Verde", emoji: "🇨🇻" },
{ value: "CW", label: "Curaçao", emoji: "🇨🇼" },
{ value: "CX", label: "Christmas Island", emoji: "🇨🇽" },
{ value: "CY", label: "Cyprus", emoji: "🇨🇾" },
{ value: "CZ", label: "Czechia", emoji: "🇨🇿" },
{ value: "DE", label: "Germany", emoji: "🇩🇪" },
{ value: "DJ", label: "Djibouti", emoji: "🇩🇯" },
{ value: "DK", label: "Denmark", emoji: "🇩🇰" },
{ value: "DM", label: "Dominica", emoji: "🇩🇲" },
{ value: "DO", label: "Dominican Republic", emoji: "🇩🇴" },
{ value: "DZ", label: "Algeria", emoji: "🇩🇿" },
{ value: "EC", label: "Ecuador", emoji: "🇪🇨" },
{ value: "EE", label: "Estonia", emoji: "🇪🇪" },
{ value: "EG", label: "Egypt", emoji: "🇪🇬" },
{ value: "EH", label: "Western Sahara", emoji: "🇪🇭" },
{ value: "ER", label: "Eritrea", emoji: "🇪🇷" },
{ value: "ES", label: "Spain", emoji: "🇪🇸" },
{ value: "ET", label: "Ethiopia", emoji: "🇪🇹" },
{ value: "FI", label: "Finland", emoji: "🇫🇮" },
{ value: "FJ", label: "Fiji", emoji: "🇫🇯" },
{ value: "FK", label: "Falkland Islands (Malvinas)", emoji: "🇫🇰" },
{ value: "FM", label: "Micronesia, Federated States of", emoji: "🇫🇲" },
{ value: "FO", label: "Faroe Islands", emoji: "🇫🇴" },
{ value: "FR", label: "France", emoji: "🇫🇷" },
{ value: "GA", label: "Gabon", emoji: "🇬🇦" },
{
value: "GB",
label: "United Kingdom of Great Britain and Northern Ireland",
emoji: "🇬🇧",
},
{ value: "GD", label: "Grenada", emoji: "🇬🇩" },
{ value: "GE", label: "Georgia", emoji: "🇬🇪" },
{ value: "GF", label: "French Guiana", emoji: "🇬🇫" },
{ value: "GG", label: "Guernsey", emoji: "🇬🇬" },
{ value: "GH", label: "Ghana", emoji: "🇬🇭" },
{ value: "GI", label: "Gibraltar", emoji: "🇬🇮" },
{ value: "GL", label: "Greenland", emoji: "🇬🇱" },
{ value: "GM", label: "Gambia", emoji: "🇬🇲" },
{ value: "GN", label: "Guinea", emoji: "🇬🇳" },
{ value: "GP", label: "Guadeloupe", emoji: "🇬🇵" },
{ value: "GQ", label: "Equatorial Guinea", emoji: "🇬🇶" },
{ value: "GR", label: "Greece", emoji: "🇬🇷" },
{
value: "GS",
label: "South Georgia and the South Sandwich Islands",
emoji: "🇬🇸",
},
{ value: "GT", label: "Guatemala", emoji: "🇬🇹" },
{ value: "GU", label: "Guam", emoji: "🇬🇺" },
{ value: "GW", label: "Guinea-Bissau", emoji: "🇬🇼" },
{ value: "GY", label: "Guyana", emoji: "🇬🇾" },
{ value: "HK", label: "Hong Kong", emoji: "🇭🇰" },
{ value: "HM", label: "Heard Island and McDonald Islands", emoji: "🇭🇲" },
{ value: "HN", label: "Honduras", emoji: "🇭🇳" },
{ value: "HR", label: "Croatia", emoji: "🇭🇷" },
{ value: "HT", label: "Haiti", emoji: "🇭🇹" },
{ value: "HU", label: "Hungary", emoji: "🇭🇺" },
{ value: "ID", label: "Indonesia", emoji: "🇮🇩" },
{ value: "IE", label: "Ireland", emoji: "🇮🇪" },
{ value: "IL", label: "Israel", emoji: "🇮🇱" },
{ value: "IM", label: "Isle of Man", emoji: "🇮🇲" },
{ value: "IN", label: "India", emoji: "🇮🇳" },
{ value: "IO", label: "British Indian Ocean Territory", emoji: "🇮🇴" },
{ value: "IQ", label: "Iraq", emoji: "🇮🇶" },
{ value: "IR", label: "Iran, Islamic Republic of", emoji: "🇮🇷" },
{ value: "IS", label: "Iceland", emoji: "🇮🇸" },
{ value: "IT", label: "Italy", emoji: "🇮🇹" },
{ value: "JE", label: "Jersey", emoji: "🇯🇪" },
{ value: "JM", label: "Jamaica", emoji: "🇯🇲" },
{ value: "JO", label: "Jordan", emoji: "🇯🇴" },
{ value: "JP", label: "Japan", emoji: "🇯🇵" },
{ value: "KE", label: "Kenya", emoji: "🇰🇪" },
{ value: "KG", label: "Kyrgyzstan", emoji: "🇰🇬" },
{ value: "KH", label: "Cambodia", emoji: "🇰🇭" },
{ value: "KI", label: "Kiribati", emoji: "🇰🇮" },
{ value: "KM", label: "Comoros", emoji: "🇰🇲" },
{ value: "KN", label: "Saint Kitts and Nevis", emoji: "🇰🇳" },
{ value: "KP", label: "Korea, Democratic People's Republic of", emoji: "🇰🇵" },
{ value: "KR", label: "Korea, Republic of", emoji: "🇰🇷" },
{ value: "KW", label: "Kuwait", emoji: "🇰🇼" },
{ value: "KY", label: "Cayman Islands", emoji: "🇰🇾" },
{ value: "KZ", label: "Kazakhstan", emoji: "🇰🇿" },
{ value: "LA", label: "Lao People's Democratic Republic", emoji: "🇱🇦" },
{ value: "LB", label: "Lebanon", emoji: "🇱🇧" },
{ value: "LC", label: "Saint Lucia", emoji: "🇱🇨" },
{ value: "LI", label: "Liechtenstein", emoji: "🇱🇮" },
{ value: "LK", label: "Sri Lanka", emoji: "🇱🇰" },
{ value: "LR", label: "Liberia", emoji: "🇱🇷" },
{ value: "LS", label: "Lesotho", emoji: "🇱🇸" },
{ value: "LT", label: "Lithuania", emoji: "🇱🇹" },
{ value: "LU", label: "Luxembourg", emoji: "🇱🇺" },
{ value: "LV", label: "Latvia", emoji: "🇱🇻" },
{ value: "LY", label: "Libya", emoji: "🇱🇾" },
{ value: "MA", label: "Morocco", emoji: "🇲🇦" },
{ value: "MC", label: "Monaco", emoji: "🇲🇨" },
{ value: "MD", label: "Moldova, Republic of", emoji: "🇲🇩" },
{ value: "ME", label: "Montenegro", emoji: "🇲🇪" },
{ value: "MF", label: "Saint Martin, (French part)", emoji: "🇲🇫" },
{ value: "MG", label: "Madagascar", emoji: "🇲🇬" },
{ value: "MH", label: "Marshall Islands", emoji: "🇲🇭" },
{ value: "MK", label: "North Macedonia", emoji: "🇲🇰" },
{ value: "ML", label: "Mali", emoji: "🇲🇱" },
{ value: "MM", label: "Myanmar", emoji: "🇲🇲" },
{ value: "MN", label: "Mongolia", emoji: "🇲🇳" },
{ value: "MO", label: "Macao", emoji: "🇲🇴" },
{ value: "MP", label: "Northern Mariana Islands", emoji: "🇲🇵" },
{ value: "MQ", label: "Martinique", emoji: "🇲🇶" },
{ value: "MR", label: "Mauritania", emoji: "🇲🇷" },
{ value: "MS", label: "Montserrat", emoji: "🇲🇸" },
{ value: "MT", label: "Malta", emoji: "🇲🇹" },
{ value: "MU", label: "Mauritius", emoji: "🇲🇺" },
{ value: "MV", label: "Maldives", emoji: "🇲🇻" },
{ value: "MW", label: "Malawi", emoji: "🇲🇼" },
{ value: "MX", label: "Mexico", emoji: "🇲🇽" },
{ value: "MY", label: "Malaysia", emoji: "🇲🇾" },
{ value: "MZ", label: "Mozambique", emoji: "🇲🇿" },
{ value: "NA", label: "Namibia", emoji: "🇳🇦" },
{ value: "NC", label: "New Caledonia", emoji: "🇳🇨" },
{ value: "NE", label: "Niger", emoji: "🇳🇪" },
{ value: "NF", label: "Norfolk Island", emoji: "🇳🇫" },
{ value: "NG", label: "Nigeria", emoji: "🇳🇬" },
{ value: "NI", label: "Nicaragua", emoji: "🇳🇮" },
{ value: "NL", label: "Netherlands", emoji: "🇳🇱" },
{ value: "NO", label: "Norway", emoji: "🇳🇴" },
{ value: "NP", label: "Nepal", emoji: "🇳🇵" },
{ value: "NR", label: "Nauru", emoji: "🇳🇷" },
{ value: "NU", label: "Niue", emoji: "🇳🇺" },
{ value: "NZ", label: "New Zealand", emoji: "🇳🇿" },
{ value: "OM", label: "Oman", emoji: "🇴🇲" },
{ value: "PA", label: "Panama", emoji: "🇵🇦" },
{ value: "PE", label: "Peru", emoji: "🇵🇪" },
{ value: "PF", label: "French Polynesia", emoji: "🇵🇫" },
{ value: "PG", label: "Papua New Guinea", emoji: "🇵🇬" },
{ value: "PH", label: "Philippines", emoji: "🇵🇭" },
{ value: "PK", label: "Pakistan", emoji: "🇵🇰" },
{ value: "PL", label: "Poland", emoji: "🇵🇱" },
{ value: "PM", label: "Saint Pierre and Miquelon", emoji: "🇵🇲" },
{ value: "PN", label: "Pitcairn", emoji: "🇵🇳" },
{ value: "PR", label: "Puerto Rico", emoji: "🇵🇷" },
{ value: "PS", label: "Palestine, State of", emoji: "🇵🇸" },
{ value: "PT", label: "Portugal", emoji: "🇵🇹" },
{ value: "PW", label: "Palau", emoji: "🇵🇼" },
{ value: "PY", label: "Paraguay", emoji: "🇵🇾" },
{ value: "QA", label: "Qatar", emoji: "🇶🇦" },
{ value: "RE", label: "Réunion", emoji: "🇷🇪" },
{ value: "RO", label: "Romania", emoji: "🇷🇴" },
{ value: "RS", label: "Serbia", emoji: "🇷🇸" },
{ value: "RU", label: "Russian Federation", emoji: "🇷🇺" },
{ value: "RW", label: "Rwanda", emoji: "🇷🇼" },
{ value: "SA", label: "Saudi Arabia", emoji: "🇸🇦" },
{ value: "SB", label: "Solomon Islands", emoji: "🇸🇧" },
{ value: "SC", label: "Seychelles", emoji: "🇸🇨" },
{ value: "SD", label: "Sudan", emoji: "🇸🇩" },
{ value: "SE", label: "Sweden", emoji: "🇸🇪" },
{ value: "SG", label: "Singapore", emoji: "🇸🇬" },
{
value: "SH",
label: "Saint Helena, Ascension and Tristan da Cunha",
emoji: "🇸🇭",
},
{ value: "SI", label: "Slovenia", emoji: "🇸🇮" },
{ value: "SJ", label: "Svalbard and Jan Mayen", emoji: "🇸🇯" },
{ value: "SK", label: "Slovakia", emoji: "🇸🇰" },
{ value: "SL", label: "Sierra Leone", emoji: "🇸🇱" },
{ value: "SM", label: "San Marino", emoji: "🇸🇲" },
{ value: "SN", label: "Senegal", emoji: "🇸🇳" },
{ value: "SO", label: "Somalia", emoji: "🇸🇴" },
{ value: "SR", label: "Suriname", emoji: "🇸🇷" },
{ value: "SS", label: "South Sudan", emoji: "🇸🇸" },
{ value: "ST", label: "Sao Tome and Principe", emoji: "🇸🇹" },
{ value: "SV", label: "El Salvador", emoji: "🇸🇻" },
{ value: "SX", label: "Sint Maarten, (Dutch part)", emoji: "🇸🇽" },
{ value: "SY", label: "Syrian Arab Republic", emoji: "🇸🇾" },
{ value: "SZ", label: "Eswatini", emoji: "🇸🇿" },
{ value: "TC", label: "Turks and Caicos Islands", emoji: "🇹🇨" },
{ value: "TD", label: "Chad", emoji: "🇹🇩" },
{ value: "TF", label: "French Southern Territories", emoji: "🇹🇫" },
{ value: "TG", label: "Togo", emoji: "🇹🇬" },
{ value: "TH", label: "Thailand", emoji: "🇹🇭" },
{ value: "TJ", label: "Tajikistan", emoji: "🇹🇯" },
{ value: "TK", label: "Tokelau", emoji: "🇹🇰" },
{ value: "TL", label: "Timor-Leste", emoji: "🇹🇱" },
{ value: "TM", label: "Turkmenistan", emoji: "🇹🇲" },
{ value: "TN", label: "Tunisia", emoji: "🇹🇳" },
{ value: "TO", label: "Tonga", emoji: "🇹🇴" },
{ value: "TR", label: "Türkiye", emoji: "🇹🇷" },
{ value: "TT", label: "Trinidad and Tobago", emoji: "🇹🇹" },
{ value: "TV", label: "Tuvalu", emoji: "🇹🇻" },
{ value: "TW", label: "Taiwan, Province of China", emoji: "🇹🇼" },
{ value: "TZ", label: "Tanzania, United Republic of", emoji: "🇹🇿" },
{ value: "UA", label: "Ukraine", emoji: "🇺🇦" },
{ value: "UG", label: "Uganda", emoji: "🇺🇬" },
{ value: "UM", label: "United States Minor Outlying Islands", emoji: "🇺🇲" },
{ value: "US", label: "United States of America", emoji: "🇺🇸" },
{ value: "UY", label: "Uruguay", emoji: "🇺🇾" },
{ value: "UZ", label: "Uzbekistan", emoji: "🇺🇿" },
{ value: "VA", label: "Holy See", emoji: "🇻🇦" },
{ value: "VC", label: "Saint Vincent and the Grenadines", emoji: "🇻🇨" },
{ value: "VE", label: "Venezuela, Bolivarian Republic of", emoji: "🇻🇪" },
{ value: "VG", label: "Virgin Islands, British", emoji: "🇻🇬" },
{ value: "VI", label: "Virgin Islands, U.S.", emoji: "🇻🇮" },
{ value: "VN", label: "Viet Nam", emoji: "🇻🇳" },
{ value: "VU", label: "Vanuatu", emoji: "🇻🇺" },
{ value: "WF", label: "Wallis and Futuna", emoji: "🇼🇫" },
{ value: "WS", label: "Samoa", emoji: "🇼🇸" },
{ value: "YE", label: "Yemen", emoji: "🇾🇪" },
{ value: "YT", label: "Mayotte", emoji: "🇾🇹" },
{ value: "ZA", label: "South Africa", emoji: "🇿🇦" },
{ value: "ZM", label: "Zambia", emoji: "🇿🇲" },
{ value: "ZW", label: "Zimbabwe", emoji: "🇿🇼" },
]
虚拟化
¥Virtualization
或者,你可以利用 @tanstack/react-virtual 包中的虚拟化功能来高效地渲染大型数据集。
¥Alternatively, you can leverage virtualization from the
@tanstack/react-virtual package to render large datasets efficiently.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { useVirtualizer } from "@tanstack/react-virtual"
import { useRef } from "react"
import { flushSync } from "react-dom"
const Demo = () => {
const contentRef = useRef<HTMLDivElement>(null)
const { startsWith } = useFilter({ sensitivity: "base" })
const { collection, filter, reset } = useListCollection({
initialItems: items,
filter: startsWith,
})
const virtualizer = useVirtualizer({
count: collection.size,
getScrollElement: () => contentRef.current,
estimateSize: () => 28,
overscan: 10,
scrollPaddingEnd: 32,
})
const handleScrollToIndexFn = (details: { index: number }) => {
flushSync(() => {
virtualizer.scrollToIndex(details.index, {
align: "center",
behavior: "auto",
})
})
}
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
scrollToIndexFn={handleScrollToIndexFn}
width="320px"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger onClick={reset} />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content ref={contentRef}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = collection.items[virtualItem.index]
return (
<Combobox.Item
key={item.value}
item={item}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
<Combobox.ItemText truncate>
<span aria-hidden style={{ marginRight: 4 }}>
{item.emoji}
</span>
{item.label}
</Combobox.ItemText>
<Combobox.ItemIndicator />
</Combobox.Item>
)
})}
</div>
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
export const items = [
{ value: "AD", label: "Andorra", emoji: "🇦🇩" },
{ value: "AE", label: "United Arab Emirates", emoji: "🇦🇪" },
{ value: "AF", label: "Afghanistan", emoji: "🇦🇫" },
{ value: "AG", label: "Antigua and Barbuda", emoji: "🇦🇬" },
{ value: "AI", label: "Anguilla", emoji: "🇦🇮" },
{ value: "AL", label: "Albania", emoji: "🇦🇱" },
{ value: "AM", label: "Armenia", emoji: "🇦🇲" },
{ value: "AO", label: "Angola", emoji: "🇦🇴" },
{ value: "AQ", label: "Antarctica", emoji: "🇦🇶" },
{ value: "AR", label: "Argentina", emoji: "🇦🇷" },
{ value: "AS", label: "American Samoa", emoji: "🇦🇸" },
{ value: "AT", label: "Austria", emoji: "🇦🇹" },
{ value: "AU", label: "Australia", emoji: "🇦🇺" },
{ value: "AW", label: "Aruba", emoji: "🇦🇼" },
{ value: "AX", label: "Åland Islands", emoji: "🇦🇽" },
{ value: "AZ", label: "Azerbaijan", emoji: "🇦🇿" },
{ value: "BA", label: "Bosnia and Herzegovina", emoji: "🇧🇦" },
{ value: "BB", label: "Barbados", emoji: "🇧🇧" },
{ value: "BD", label: "Bangladesh", emoji: "🇧🇩" },
{ value: "BE", label: "Belgium", emoji: "🇧🇪" },
{ value: "BF", label: "Burkina Faso", emoji: "🇧🇫" },
{ value: "BG", label: "Bulgaria", emoji: "🇧🇬" },
{ value: "BH", label: "Bahrain", emoji: "🇧🇭" },
{ value: "BI", label: "Burundi", emoji: "🇧🇮" },
{ value: "BJ", label: "Benin", emoji: "🇧🇯" },
{ value: "BL", label: "Saint Barthélemy", emoji: "🇧🇱" },
{ value: "BM", label: "Bermuda", emoji: "🇧🇲" },
{ value: "BN", label: "Brunei Darussalam", emoji: "🇧🇳" },
{ value: "BO", label: "Bolivia, Plurinational State of", emoji: "🇧🇴" },
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba", emoji: "🇧🇶" },
{ value: "BR", label: "Brazil", emoji: "🇧🇷" },
{ value: "BS", label: "Bahamas", emoji: "🇧🇸" },
{ value: "BT", label: "Bhutan", emoji: "🇧🇹" },
{ value: "BV", label: "Bouvet Island", emoji: "🇧🇻" },
{ value: "BW", label: "Botswana", emoji: "🇧🇼" },
{ value: "BY", label: "Belarus", emoji: "🇧🇾" },
{ value: "BZ", label: "Belize", emoji: "🇧🇿" },
{ value: "CA", label: "Canada", emoji: "🇨🇦" },
{ value: "CC", label: "Cocos (Keeling) Islands", emoji: "🇨🇨" },
{ value: "CD", label: "Congo, Democratic Republic of the", emoji: "🇨🇩" },
{ value: "CF", label: "Central African Republic", emoji: "🇨🇫" },
{ value: "CG", label: "Congo", emoji: "🇨🇬" },
{ value: "CH", label: "Switzerland", emoji: "🇨🇭" },
{ value: "CI", label: "Côte d'Ivoire", emoji: "🇨🇮" },
{ value: "CK", label: "Cook Islands", emoji: "🇨🇰" },
{ value: "CL", label: "Chile", emoji: "🇨🇱" },
{ value: "CM", label: "Cameroon", emoji: "🇨🇲" },
{ value: "CN", label: "China", emoji: "🇨🇳" },
{ value: "CO", label: "Colombia", emoji: "🇨🇴" },
{ value: "CR", label: "Costa Rica", emoji: "🇨🇷" },
{ value: "CU", label: "Cuba", emoji: "🇨🇺" },
{ value: "CV", label: "Cabo Verde", emoji: "🇨🇻" },
{ value: "CW", label: "Curaçao", emoji: "🇨🇼" },
{ value: "CX", label: "Christmas Island", emoji: "🇨🇽" },
{ value: "CY", label: "Cyprus", emoji: "🇨🇾" },
{ value: "CZ", label: "Czechia", emoji: "🇨🇿" },
{ value: "DE", label: "Germany", emoji: "🇩🇪" },
{ value: "DJ", label: "Djibouti", emoji: "🇩🇯" },
{ value: "DK", label: "Denmark", emoji: "🇩🇰" },
{ value: "DM", label: "Dominica", emoji: "🇩🇲" },
{ value: "DO", label: "Dominican Republic", emoji: "🇩🇴" },
{ value: "DZ", label: "Algeria", emoji: "🇩🇿" },
{ value: "EC", label: "Ecuador", emoji: "🇪🇨" },
{ value: "EE", label: "Estonia", emoji: "🇪🇪" },
{ value: "EG", label: "Egypt", emoji: "🇪🇬" },
{ value: "EH", label: "Western Sahara", emoji: "🇪🇭" },
{ value: "ER", label: "Eritrea", emoji: "🇪🇷" },
{ value: "ES", label: "Spain", emoji: "🇪🇸" },
{ value: "ET", label: "Ethiopia", emoji: "🇪🇹" },
{ value: "FI", label: "Finland", emoji: "🇫🇮" },
{ value: "FJ", label: "Fiji", emoji: "🇫🇯" },
{ value: "FK", label: "Falkland Islands (Malvinas)", emoji: "🇫🇰" },
{ value: "FM", label: "Micronesia, Federated States of", emoji: "🇫🇲" },
{ value: "FO", label: "Faroe Islands", emoji: "🇫🇴" },
{ value: "FR", label: "France", emoji: "🇫🇷" },
{ value: "GA", label: "Gabon", emoji: "🇬🇦" },
{
value: "GB",
label: "United Kingdom of Great Britain and Northern Ireland",
emoji: "🇬🇧",
},
{ value: "GD", label: "Grenada", emoji: "🇬🇩" },
{ value: "GE", label: "Georgia", emoji: "🇬🇪" },
{ value: "GF", label: "French Guiana", emoji: "🇬🇫" },
{ value: "GG", label: "Guernsey", emoji: "🇬🇬" },
{ value: "GH", label: "Ghana", emoji: "🇬🇭" },
{ value: "GI", label: "Gibraltar", emoji: "🇬🇮" },
{ value: "GL", label: "Greenland", emoji: "🇬🇱" },
{ value: "GM", label: "Gambia", emoji: "🇬🇲" },
{ value: "GN", label: "Guinea", emoji: "🇬🇳" },
{ value: "GP", label: "Guadeloupe", emoji: "🇬🇵" },
{ value: "GQ", label: "Equatorial Guinea", emoji: "🇬🇶" },
{ value: "GR", label: "Greece", emoji: "🇬🇷" },
{
value: "GS",
label: "South Georgia and the South Sandwich Islands",
emoji: "🇬🇸",
},
{ value: "GT", label: "Guatemala", emoji: "🇬🇹" },
{ value: "GU", label: "Guam", emoji: "🇬🇺" },
{ value: "GW", label: "Guinea-Bissau", emoji: "🇬🇼" },
{ value: "GY", label: "Guyana", emoji: "🇬🇾" },
{ value: "HK", label: "Hong Kong", emoji: "🇭🇰" },
{ value: "HM", label: "Heard Island and McDonald Islands", emoji: "🇭🇲" },
{ value: "HN", label: "Honduras", emoji: "🇭🇳" },
{ value: "HR", label: "Croatia", emoji: "🇭🇷" },
{ value: "HT", label: "Haiti", emoji: "🇭🇹" },
{ value: "HU", label: "Hungary", emoji: "🇭🇺" },
{ value: "ID", label: "Indonesia", emoji: "🇮🇩" },
{ value: "IE", label: "Ireland", emoji: "🇮🇪" },
{ value: "IL", label: "Israel", emoji: "🇮🇱" },
{ value: "IM", label: "Isle of Man", emoji: "🇮🇲" },
{ value: "IN", label: "India", emoji: "🇮🇳" },
{ value: "IO", label: "British Indian Ocean Territory", emoji: "🇮🇴" },
{ value: "IQ", label: "Iraq", emoji: "🇮🇶" },
{ value: "IR", label: "Iran, Islamic Republic of", emoji: "🇮🇷" },
{ value: "IS", label: "Iceland", emoji: "🇮🇸" },
{ value: "IT", label: "Italy", emoji: "🇮🇹" },
{ value: "JE", label: "Jersey", emoji: "🇯🇪" },
{ value: "JM", label: "Jamaica", emoji: "🇯🇲" },
{ value: "JO", label: "Jordan", emoji: "🇯🇴" },
{ value: "JP", label: "Japan", emoji: "🇯🇵" },
{ value: "KE", label: "Kenya", emoji: "🇰🇪" },
{ value: "KG", label: "Kyrgyzstan", emoji: "🇰🇬" },
{ value: "KH", label: "Cambodia", emoji: "🇰🇭" },
{ value: "KI", label: "Kiribati", emoji: "🇰🇮" },
{ value: "KM", label: "Comoros", emoji: "🇰🇲" },
{ value: "KN", label: "Saint Kitts and Nevis", emoji: "🇰🇳" },
{ value: "KP", label: "Korea, Democratic People's Republic of", emoji: "🇰🇵" },
{ value: "KR", label: "Korea, Republic of", emoji: "🇰🇷" },
{ value: "KW", label: "Kuwait", emoji: "🇰🇼" },
{ value: "KY", label: "Cayman Islands", emoji: "🇰🇾" },
{ value: "KZ", label: "Kazakhstan", emoji: "🇰🇿" },
{ value: "LA", label: "Lao People's Democratic Republic", emoji: "🇱🇦" },
{ value: "LB", label: "Lebanon", emoji: "🇱🇧" },
{ value: "LC", label: "Saint Lucia", emoji: "🇱🇨" },
{ value: "LI", label: "Liechtenstein", emoji: "🇱🇮" },
{ value: "LK", label: "Sri Lanka", emoji: "🇱🇰" },
{ value: "LR", label: "Liberia", emoji: "🇱🇷" },
{ value: "LS", label: "Lesotho", emoji: "🇱🇸" },
{ value: "LT", label: "Lithuania", emoji: "🇱🇹" },
{ value: "LU", label: "Luxembourg", emoji: "🇱🇺" },
{ value: "LV", label: "Latvia", emoji: "🇱🇻" },
{ value: "LY", label: "Libya", emoji: "🇱🇾" },
{ value: "MA", label: "Morocco", emoji: "🇲🇦" },
{ value: "MC", label: "Monaco", emoji: "🇲🇨" },
{ value: "MD", label: "Moldova, Republic of", emoji: "🇲🇩" },
{ value: "ME", label: "Montenegro", emoji: "🇲🇪" },
{ value: "MF", label: "Saint Martin, (French part)", emoji: "🇲🇫" },
{ value: "MG", label: "Madagascar", emoji: "🇲🇬" },
{ value: "MH", label: "Marshall Islands", emoji: "🇲🇭" },
{ value: "MK", label: "North Macedonia", emoji: "🇲🇰" },
{ value: "ML", label: "Mali", emoji: "🇲🇱" },
{ value: "MM", label: "Myanmar", emoji: "🇲🇲" },
{ value: "MN", label: "Mongolia", emoji: "🇲🇳" },
{ value: "MO", label: "Macao", emoji: "🇲🇴" },
{ value: "MP", label: "Northern Mariana Islands", emoji: "🇲🇵" },
{ value: "MQ", label: "Martinique", emoji: "🇲🇶" },
{ value: "MR", label: "Mauritania", emoji: "🇲🇷" },
{ value: "MS", label: "Montserrat", emoji: "🇲🇸" },
{ value: "MT", label: "Malta", emoji: "🇲🇹" },
{ value: "MU", label: "Mauritius", emoji: "🇲🇺" },
{ value: "MV", label: "Maldives", emoji: "🇲🇻" },
{ value: "MW", label: "Malawi", emoji: "🇲🇼" },
{ value: "MX", label: "Mexico", emoji: "🇲🇽" },
{ value: "MY", label: "Malaysia", emoji: "🇲🇾" },
{ value: "MZ", label: "Mozambique", emoji: "🇲🇿" },
{ value: "NA", label: "Namibia", emoji: "🇳🇦" },
{ value: "NC", label: "New Caledonia", emoji: "🇳🇨" },
{ value: "NE", label: "Niger", emoji: "🇳🇪" },
{ value: "NF", label: "Norfolk Island", emoji: "🇳🇫" },
{ value: "NG", label: "Nigeria", emoji: "🇳🇬" },
{ value: "NI", label: "Nicaragua", emoji: "🇳🇮" },
{ value: "NL", label: "Netherlands", emoji: "🇳🇱" },
{ value: "NO", label: "Norway", emoji: "🇳🇴" },
{ value: "NP", label: "Nepal", emoji: "🇳🇵" },
{ value: "NR", label: "Nauru", emoji: "🇳🇷" },
{ value: "NU", label: "Niue", emoji: "🇳🇺" },
{ value: "NZ", label: "New Zealand", emoji: "🇳🇿" },
{ value: "OM", label: "Oman", emoji: "🇴🇲" },
{ value: "PA", label: "Panama", emoji: "🇵🇦" },
{ value: "PE", label: "Peru", emoji: "🇵🇪" },
{ value: "PF", label: "French Polynesia", emoji: "🇵🇫" },
{ value: "PG", label: "Papua New Guinea", emoji: "🇵🇬" },
{ value: "PH", label: "Philippines", emoji: "🇵🇭" },
{ value: "PK", label: "Pakistan", emoji: "🇵🇰" },
{ value: "PL", label: "Poland", emoji: "🇵🇱" },
{ value: "PM", label: "Saint Pierre and Miquelon", emoji: "🇵🇲" },
{ value: "PN", label: "Pitcairn", emoji: "🇵🇳" },
{ value: "PR", label: "Puerto Rico", emoji: "🇵🇷" },
{ value: "PS", label: "Palestine, State of", emoji: "🇵🇸" },
{ value: "PT", label: "Portugal", emoji: "🇵🇹" },
{ value: "PW", label: "Palau", emoji: "🇵🇼" },
{ value: "PY", label: "Paraguay", emoji: "🇵🇾" },
{ value: "QA", label: "Qatar", emoji: "🇶🇦" },
{ value: "RE", label: "Réunion", emoji: "🇷🇪" },
{ value: "RO", label: "Romania", emoji: "🇷🇴" },
{ value: "RS", label: "Serbia", emoji: "🇷🇸" },
{ value: "RU", label: "Russian Federation", emoji: "🇷🇺" },
{ value: "RW", label: "Rwanda", emoji: "🇷🇼" },
{ value: "SA", label: "Saudi Arabia", emoji: "🇸🇦" },
{ value: "SB", label: "Solomon Islands", emoji: "🇸🇧" },
{ value: "SC", label: "Seychelles", emoji: "🇸🇨" },
{ value: "SD", label: "Sudan", emoji: "🇸🇩" },
{ value: "SE", label: "Sweden", emoji: "🇸🇪" },
{ value: "SG", label: "Singapore", emoji: "🇸🇬" },
{
value: "SH",
label: "Saint Helena, Ascension and Tristan da Cunha",
emoji: "🇸🇭",
},
{ value: "SI", label: "Slovenia", emoji: "🇸🇮" },
{ value: "SJ", label: "Svalbard and Jan Mayen", emoji: "🇸🇯" },
{ value: "SK", label: "Slovakia", emoji: "🇸🇰" },
{ value: "SL", label: "Sierra Leone", emoji: "🇸🇱" },
{ value: "SM", label: "San Marino", emoji: "🇸🇲" },
{ value: "SN", label: "Senegal", emoji: "🇸🇳" },
{ value: "SO", label: "Somalia", emoji: "🇸🇴" },
{ value: "SR", label: "Suriname", emoji: "🇸🇷" },
{ value: "SS", label: "South Sudan", emoji: "🇸🇸" },
{ value: "ST", label: "Sao Tome and Principe", emoji: "🇸🇹" },
{ value: "SV", label: "El Salvador", emoji: "🇸🇻" },
{ value: "SX", label: "Sint Maarten, (Dutch part)", emoji: "🇸🇽" },
{ value: "SY", label: "Syrian Arab Republic", emoji: "🇸🇾" },
{ value: "SZ", label: "Eswatini", emoji: "🇸🇿" },
{ value: "TC", label: "Turks and Caicos Islands", emoji: "🇹🇨" },
{ value: "TD", label: "Chad", emoji: "🇹🇩" },
{ value: "TF", label: "French Southern Territories", emoji: "🇹🇫" },
{ value: "TG", label: "Togo", emoji: "🇹🇬" },
{ value: "TH", label: "Thailand", emoji: "🇹🇭" },
{ value: "TJ", label: "Tajikistan", emoji: "🇹🇯" },
{ value: "TK", label: "Tokelau", emoji: "🇹🇰" },
{ value: "TL", label: "Timor-Leste", emoji: "🇹🇱" },
{ value: "TM", label: "Turkmenistan", emoji: "🇹🇲" },
{ value: "TN", label: "Tunisia", emoji: "🇹🇳" },
{ value: "TO", label: "Tonga", emoji: "🇹🇴" },
{ value: "TR", label: "Türkiye", emoji: "🇹🇷" },
{ value: "TT", label: "Trinidad and Tobago", emoji: "🇹🇹" },
{ value: "TV", label: "Tuvalu", emoji: "🇹🇻" },
{ value: "TW", label: "Taiwan, Province of China", emoji: "🇹🇼" },
{ value: "TZ", label: "Tanzania, United Republic of", emoji: "🇹🇿" },
{ value: "UA", label: "Ukraine", emoji: "🇺🇦" },
{ value: "UG", label: "Uganda", emoji: "🇺🇬" },
{ value: "UM", label: "United States Minor Outlying Islands", emoji: "🇺🇲" },
{ value: "US", label: "United States of America", emoji: "🇺🇸" },
{ value: "UY", label: "Uruguay", emoji: "🇺🇾" },
{ value: "UZ", label: "Uzbekistan", emoji: "🇺🇿" },
{ value: "VA", label: "Holy See", emoji: "🇻🇦" },
{ value: "VC", label: "Saint Vincent and the Grenadines", emoji: "🇻🇨" },
{ value: "VE", label: "Venezuela, Bolivarian Republic of", emoji: "🇻🇪" },
{ value: "VG", label: "Virgin Islands, British", emoji: "🇻🇬" },
{ value: "VI", label: "Virgin Islands, U.S.", emoji: "🇻🇮" },
{ value: "VN", label: "Viet Nam", emoji: "🇻🇳" },
{ value: "VU", label: "Vanuatu", emoji: "🇻🇺" },
{ value: "WF", label: "Wallis and Futuna", emoji: "🇼🇫" },
{ value: "WS", label: "Samoa", emoji: "🇼🇸" },
{ value: "YE", label: "Yemen", emoji: "🇾🇪" },
{ value: "YT", label: "Mayotte", emoji: "🇾🇹" },
{ value: "ZA", label: "South Africa", emoji: "🇿🇦" },
{ value: "ZM", label: "Zambia", emoji: "🇿🇲" },
{ value: "ZW", label: "Zimbabwe", emoji: "🇿🇼" },
]
链接
¥Links
使用 asChild 属性将组合框项渲染为链接。
¥Use the asChild prop to render the combobox items as links.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
import { LuExternalLink } from "react-icons/lu"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
selectionBehavior="clear"
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item asChild item={item} key={item.value}>
<a href={item.docs}>
{item.label} <LuExternalLink size={10} />
</a>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react", docs: "https://react.dev" },
{ label: "Solid", value: "solid", docs: "https://solidjs.com" },
{ label: "Vue", value: "vue", docs: "https://vuejs.org" },
{ label: "Angular", value: "angular", docs: "https://angular.io" },
{ label: "Svelte", value: "svelte", docs: "https://svelte.dev" },
{ label: "Preact", value: "preact", docs: "https://preactjs.com" },
{ label: "Qwik", value: "qwik", docs: "https://qwik.builder.io" },
{ label: "Lit", value: "lit", docs: "https://lit.dev" },
{ label: "Alpine.js", value: "alpinejs", docs: "https://alpinejs.dev" },
{ label: "Ember", value: "ember", docs: "https://emberjs.com" },
{ label: "Next.js", value: "nextjs", docs: "https://nextjs.org" },
]
对于自定义路由链接,你可以在 Combobox.Root 组件上自定义 navigate 属性。
¥For custom router links, you can customize the navigate prop on the
Combobox.Root component.
以下是使用 Tanstack 路由的示例。
¥Here's an example of using the Tanstack Router.
import { Combobox } from "@chakra-ui/react"
import { useNavigate } from "@tanstack/react-router"
function Demo() {
const navigate = useNavigate()
return (
<Combobox.Root
navigate={({ href }) => {
navigate({ to: href })
}}
>
{/* ... */}
</Combobox.Root>
)
}
补充值
¥Rehydrate Value
在某些情况下,如果组合框包含 defaultValue 但集合尚未加载,则以下示例将说明如何重新填充值并填充输入值。
¥In some cases, where a combobox has a defaultValue but the collection is not
loaded yet, here's an example of how to rehydrate the value and populate the
input value.
"use client"
import {
Combobox,
For,
HStack,
Portal,
Span,
Spinner,
useCombobox,
useListCollection,
} from "@chakra-ui/react"
import { useRef, useState } from "react"
import { useAsync } from "react-use"
const Demo = () => {
const [inputValue, setInputValue] = useState("")
const { collection, set } = useListCollection<Character>({
initialItems: [],
itemToString: (item) => item.name,
itemToValue: (item) => item.name,
})
const combobox = useCombobox({
collection,
defaultValue: ["C-3PO"],
placeholder: "Example: Dexter",
inputValue,
onInputValueChange: (e) => setInputValue(e.inputValue),
})
const state = useAsync(async () => {
const response = await fetch(
`https://swapi.py4e.com/api/people/?search=${inputValue}`,
)
const data = await response.json()
set(data.results)
}, [inputValue, set])
// Rehydrate the value
const hydrated = useRef(false)
if (combobox.value.length && collection.size && !hydrated.current) {
combobox.syncSelectedItems()
hydrated.current = true
}
return (
<Combobox.RootProvider value={combobox} width="320px">
<Combobox.Label>Search Star Wars Characters</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
{state.loading ? (
<HStack p="2">
<Spinner size="xs" />
<Span>Loading...</Span>
</HStack>
) : state.error ? (
<Span p="2" color="fg.error">
{state.error.message}
</Span>
) : (
<For
each={collection.items}
fallback={<Combobox.Empty>No items</Combobox.Empty>}
>
{(item) => (
<Combobox.Item key={item.name} item={item}>
<HStack justify="space-between" textStyle="sm">
<Span fontWeight="medium">{item.name}</Span>
<Span color="fg.muted">
{item.height}cm / {item.mass}kg
</Span>
</HStack>
<Combobox.ItemIndicator />
</Combobox.Item>
)}
</For>
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
)
}
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
自定义项目
¥Custom Item
使用你自己的组件自定义下拉菜单中项目的外观。
¥Customize the appearance of items in the dropdown with your own components.
Audi
BMW
Citroen
Dacia
Fiat
Ford
Ferrari
Honda
Hyundai
Jaguar
Jeep
Kia
Land Rover
Mazda
Mercedes
Mini
Mitsubishi
Nissan
Opel
Peugeot
Porsche
Renault
Saab
Skoda
Subaru
Suzuki
Toyota
Volkswagen
Volvo"use client"
import {
Combobox,
HStack,
Image,
Portal,
Span,
Stack,
useComboboxContext,
useFilter,
useListCollection,
} from "@chakra-ui/react"
function ComboboxValue() {
const combobox = useComboboxContext()
const selectedItems = combobox.selectedItems as (typeof items)[number][]
return (
<Stack mt="2">
{selectedItems.map((item) => (
<HStack key={item.value} textStyle="sm" p="1" borderWidth="1px">
<Image
boxSize="10"
p="2"
src={item.logo}
alt={item.label + " logo"}
/>
<span>{item.label}</span>
</HStack>
))}
</Stack>
)
}
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: items,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
placeholder="Example: Audi"
multiple
closeOnSelect
>
<Combobox.Label>Search and select car brands</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<ComboboxValue />
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
<Image boxSize="5" src={item.logo} alt={item.label + " logo"} />
<Span flex="1">{item.label}</Span>
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
export const items = [
{
label: "Audi",
value: "audi",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/audi-logo.png",
},
{
label: "BMW",
value: "bmw",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/bmw-logo.png",
},
{
label: "Citroen",
value: "citroen",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/citroen-logo.png",
},
{
label: "Dacia",
value: "dacia",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/dacia-logo.png",
},
{
label: "Fiat",
value: "fiat",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/fiat-logo.png",
},
{
label: "Ford",
value: "ford",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/ford-logo.png",
},
{
label: "Ferrari",
value: "ferrari",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/ferrari-logo.png",
},
{
label: "Honda",
value: "honda",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/honda-logo.png",
},
{
label: "Hyundai",
value: "hyundai",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/hyundai-logo.png",
},
{
label: "Jaguar",
value: "jaguar",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/jaguar-logo.png",
},
{
label: "Jeep",
value: "jeep",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/jeep-logo.png",
},
{
label: "Kia",
value: "kia",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/kia-logo.png",
},
{
label: "Land Rover",
value: "land rover",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/land-rover-logo.png",
},
{
label: "Mazda",
value: "mazda",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mazda-logo.png",
},
{
label: "Mercedes",
value: "mercedes",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mercedes-logo.png",
},
{
label: "Mini",
value: "mini",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mini-logo.png",
},
{
label: "Mitsubishi",
value: "mitsubishi",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/mitsubishi-logo.png",
},
{
label: "Nissan",
value: "nissan",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/nissan-logo.png",
},
{
label: "Opel",
value: "opel",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/opel-logo.png",
},
{
label: "Peugeot",
value: "peugeot",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/peugeot-logo.png",
},
{
label: "Porsche",
value: "porsche",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/porsche-logo.png",
},
{
label: "Renault",
value: "renault",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/renault-logo.png",
},
{
label: "Saab",
value: "saab",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/saab-logo.png",
},
{
label: "Skoda",
value: "skoda",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/skoda-logo.png",
},
{
label: "Subaru",
value: "subaru",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/subaru-logo.png",
},
{
label: "Suzuki",
value: "suzuki",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/suzuki-logo.png",
},
{
label: "Toyota",
value: "toyota",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/toyota-logo.png",
},
{
label: "Volkswagen",
value: "volkswagen",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/volkswagen-logo.png",
},
{
label: "Volvo",
value: "volvo",
logo: "https://s3.amazonaws.com/cdn.formk.it/example-assets/car-brands/volvo-logo.png",
},
]
自定义滤镜
¥Custom Filter
以下是匹配项目多个属性的自定义过滤器的示例。
¥Here's an example of a custom filter that matches multiple properties of an item.
"use client"
import {
Combobox,
Portal,
Span,
Stack,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { collection, set } = useListCollection({
initialItems: people,
itemToString: (item) => item.name,
itemToValue: (item) => item.id.toString(),
})
const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
const filteredItems = people.filter((item) => {
const searchLower = details.inputValue.toLowerCase()
const nameParts = item.name.toLowerCase().split(" ")
const emailParts = item.email.toLowerCase().split("@")[0].split(".")
return (
item.name.toLowerCase().includes(searchLower) ||
nameParts.some((part) => part.includes(searchLower)) ||
emailParts.some((part) => part.includes(searchLower)) ||
item.role.toLowerCase().includes(searchLower)
)
})
set(filteredItems)
}
return (
<Combobox.Root
width="320px"
collection={collection}
inputBehavior="autocomplete"
placeholder="Search by name, email, or role..."
onInputValueChange={handleInputChange}
>
<Combobox.Label>Select Person</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No matches found</Combobox.Empty>
{collection.items.map((person) => (
<Combobox.Item item={person} key={person.id}>
<Stack gap={0}>
<Span textStyle="sm" fontWeight="medium">
{person.name}
</Span>
<Span textStyle="xs" color="fg.muted">
{person.email}
</Span>
</Stack>
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const people = [
{
id: 1,
name: "John Smith",
email: "john@example.com",
role: "Sales Manager",
},
{
id: 2,
name: "Sarah Johnson",
email: "sarah@example.com",
role: "UI Designer",
},
{
id: 3,
name: "Michael Brown",
email: "michael@example.com",
role: "Software Engineer",
},
{
id: 4,
name: "Emily Davis",
email: "emily@example.com",
role: "AI Engineer",
},
{
id: 5,
name: "James Wilson",
email: "james@example.com",
role: "Chief Executive Officer",
},
]
自定义动画
¥Custom Animation
要自定义组合框的动画,请将 _open 和 _closed 属性传递给 Combobox.Content 组件。
¥To customize the animation of the combobox, pass the _open and _closed prop
to the Combobox.Content component.
"use client"
import {
Combobox,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
width="320px"
positioning={{ flip: false, gutter: 2 }}
>
<Combobox.Label>Select framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content
_open={{ animationStyle: "scale-fade-in" }}
_closed={{
animationStyle: "scale-fade-out",
animationDuration: "fast",
}}
>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
在对话框中
¥Within Dialog
要在对话框或弹出窗口组件中使用组合框,请避免将 Combobox.Positioner 封装在 Portal 中。
¥To use the combobox within a dialog or popover component, avoid wrapping the
Combobox.Positioner within the Portal.
-<Portal>
<Combobox.Positioner>
<Combobox.Content>
{/* ... */}
</Combobox.Content>
</Combobox.Positioner>
-</Portal>
如果你使用 Dialog 并设置了 scrollBehavior="inside",则需要:
¥If you use a Dialog and have set scrollBehavior="inside", you need to:
-
将组合框定位设置为
fixed,以避免组合框被对话框裁剪。 -
将
hideWhenDetached设置为true,以便在触发器滚动出视图时隐藏组合框。
<Combobox.Root positioning={{ strategy: "fixed", hideWhenDetached: true }}>
{/* ... */}
</Combobox.Root>
"use client"
import {
Button,
Combobox,
Popover,
Portal,
useFilter,
useListCollection,
} from "@chakra-ui/react"
const Demo = () => {
return (
<Popover.Root size="xs">
<Popover.Trigger asChild>
<Button variant="outline" size="sm">
Toggle popover
</Button>
</Popover.Trigger>
<Portal>
<Popover.Positioner>
<Popover.Content>
<Popover.Header>Select framework</Popover.Header>
<Popover.Body>
<ComboboxDemo />
</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Portal>
</Popover.Root>
)
}
const ComboboxDemo = () => {
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: frameworks,
filter: contains,
})
return (
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
)
}
const frameworks = [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Preact", value: "preact" },
{ label: "Qwik", value: "qwik" },
{ label: "Lit", value: "lit" },
{ label: "Alpine.js", value: "alpinejs" },
{ label: "Ember", value: "ember" },
{ label: "Next.js", value: "nextjs" },
]
属性
¥Props
根元素
¥Root
| Prop | Default | Type |
|---|---|---|
collection * | ListCollection<T>The collection of items | |
composite | true | booleanWhether the combobox is a composed with other composite widgets like tabs |
defaultInputValue | '\'\'' | stringThe initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
defaultValue | '[]' | string[]The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. |
inputBehavior | '\'none\'' | 'none' | 'autohighlight' | 'autocomplete'Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated |
lazyMount | false | booleanWhether to enable lazy mounting |
loopFocus | true | booleanWhether to loop the keyboard navigation through the items |
openOnChange | true | boolean | ((details: InputValueChangeDetails) => boolean)Whether to show the combobox when the input value changes |
openOnClick | false | booleanWhether to open the combobox popup on initial click on the input |
openOnKeyPress | true | booleanWhether to open the combobox on arrow key press |
positioning | '{ placement: \'bottom-start\' }' | PositioningOptionsThe positioning options to dynamically position the menu |
selectionBehavior | '\'replace\'' | 'replace' | 'clear' | 'preserve'The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
unmountOnExit | false | booleanWhether to unmount on exit. |
colorPalette | 'gray' | 'gray' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'blue' | 'cyan' | 'purple' | 'pink'The color palette of the component |
variant | 'outline' | 'outline' | 'subtle' | 'flushed'The variant of the component |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg'The size of the component |
as | React.ElementTypeThe underlying element to render. | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
unstyled | booleanWhether to remove the component's style. | |
allowCustomValue | booleanWhether to allow typing custom values in the input | |
autoFocus | booleanWhether to autofocus the input on mount | |
closeOnSelect | booleanWhether to close the combobox when an item is selected. | |
defaultHighlightedValue | stringThe initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. | |
defaultOpen | booleanThe initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. | |
disabled | booleanWhether the combobox is disabled | |
disableLayer | booleanWhether to disable registering this a dismissable layer | |
form | stringThe associate form of the combobox. | |
highlightedValue | stringThe controlled highlighted value of the combobox | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
label: string
control: string
input: string
content: string
trigger: string
clearTrigger: string
item: (id: string, index?: number | undefined) => string
positioner: string
itemGroup: (id: string | number) => string
itemGroupLabel: (id: string | number) => string
}>The ids of the elements in the combobox. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
inputValue | stringThe controlled value of the combobox's input | |
invalid | booleanWhether the combobox is invalid | |
multiple | booleanWhether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. | |
name | stringThe `name` attribute of the combobox's input. Useful for form submission | |
navigate | (details: NavigateDetails) => voidFunction to navigate to the selected item | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails<T>) => voidFunction called when an item is highlighted using the pointer or keyboard navigation. | |
onInputValueChange | (details: InputValueChangeDetails) => voidFunction called when the input's value changes | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => voidFunction called when the popup is opened | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onSelect | (details: SelectionDetails) => voidFunction called when an item is selected | |
onValueChange | (details: ValueChangeDetails<T>) => voidFunction called when a new item is selected | |
open | booleanThe controlled open state of the combobox | |
placeholder | stringThe placeholder text of the combobox's input | |
present | booleanWhether the node is present (controlled by the user) | |
readOnly | booleanWhether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it | |
required | booleanWhether the combobox is required | |
scrollToIndexFn | (details: ScrollToIndexDetails) => voidFunction to scroll to a specific index | |
translations | IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states | |
value | string[]The controlled value of the combobox's selected items |
项目
¥Item
| Prop | Default | Type |
|---|---|---|
as | React.ElementTypeThe underlying element to render. | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. For more details, read our Composition guide. | |
item | anyThe item to render | |
persistFocus | booleanWhether hovering outside should clear the highlighted state |