"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" },
]
用法
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>
要设置组合框,您可能需要导入以下 Hook
-
useListCollection
: 用于管理组合框中的项目列表,提供用于过滤和修改列表的实用方法。 -
useFilter
: 用于根据Intl.Collator
API 为组合框提供过滤逻辑。
示例
基本
基本组合框提供了一个可搜索的下拉菜单,支持单选。
"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" },
]
尺寸
将 size
属性传递给 Combobox.Root
以改变组合框的尺寸。
"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" },
]
变体
将 variant
属性传递给 Combobox.Root
以改变组合框的外观。
"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
属性传递给 Combobox.Root
以启用多选。这允许用户从列表中选择多个项目。
当设置此项时,组合框在选择项目后将始终清除输入值。
"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>
)
}
异步加载
这是一个用户输入时异步加载 collection
的示例,非常适合 API 驱动的搜索界面。
"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
}
高亮匹配文本
这是一个组合 Combobox.Item
和 Highlight
组件以在高亮搜索结果中匹配文本的示例。
"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" },
]
点击打开
使用 openOnClick
属性,在用户点击输入框时打开组合框。
"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" },
]
自定义对象
默认情况下,组合框集合期望一个包含 label
和 value
属性的对象数组。在某些情况下,您可能需要处理自定义对象。
使用 itemToString
和 itemToValue
属性将自定义对象映射到所需的接口。
const items = [
{ country: "United States", code: "US", flag: "🇺🇸" },
{ country: "Canada", code: "CA", flag: "🇨🇦" },
{ country: "Australia", code: "AU", flag: "🇦🇺" },
// ...
]
const { collection } = useListCollection({
initialItems: items,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
})
最少字符数
使用 openOnChange
属性设置过滤列表前的最少字符数。
<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" },
]
字段
将 Combobox
组件与 Field
组件组合,将组合框包装在表单字段中。这对于表单布局很有用。
"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" },
]
禁用状态
将 disabled
属性传递给 Combobox.Root
以禁用整个组合框。
"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
属性添加到集合项。
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>
),
},
]
输入组
与 InputGroup 结合使用以添加图标或其他元素。
"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
属性传递给 Combobox.Root
以显示错误状态。
"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" },
]
受控值
使用 value
和 onValueChange
属性以编程方式控制组合框的值。
"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" },
]
存储
控制组合框的另一种方法是使用 Combobox.RootProvider
组件和 useCombobox
存储 Hook。
import { Combobox, useCombobox } from "@chakra-ui/react"
function Demo() {
const combobox = useCombobox()
return (
<Combobox.RootProvider value={combobox}>{/* ... */}</Combobox.RootProvider>
)
}
这样您就可以从组合框外部访问组合框的状态和方法。
"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" },
]
受控打开状态
使用 open
和 onOpenChange
属性以编程方式控制组合框的打开状态。
"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" },
]
限制大数据集
管理大型列表的推荐方法是在 useListCollection
Hook 上使用 limit
属性。这将限制 DOM 中渲染的项目数量以提高性能。
"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: "🇿🇼" },
]
虚拟化
另外,您可以利用 @tanstack/react-virtual
包的虚拟化功能,高效渲染大型数据集。
"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: "🇿🇼" },
]
链接
使用 asChild
属性将组合框项目渲染为链接。
"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://reactjs.ac.cn" },
{ label: "Solid", value: "solid", docs: "https://solidjs.com" },
{ label: "Vue", value: "vue", docs: "https://vuejs.net.cn" },
{ label: "Angular", value: "angular", docs: "https://angular.io" },
{ label: "Svelte", value: "svelte", docs: "https://svelte.net.cn" },
{ label: "Preact", value: "preact", docs: "https://preact.reactjs.ac.cn" },
{ label: "Qwik", value: "qwik", docs: "https://qwik.node.org.cn" },
{ label: "Lit", value: "lit", docs: "https://lit.npmjs.net.cn" },
{ 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.net.cn" },
]
对于自定义路由链接,您可以自定义 Combobox.Root
组件上的 navigate
属性。
这是一个使用 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>
)
}
恢复值
在某些情况下,组合框具有 defaultValue
但集合尚未加载时,这是一个如何恢复值并填充输入值的示例。
"use client"
import {
Combobox,
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) {
const inputValue = collection.stringify(combobox.value[0])
combobox.setInputValue(inputValue || "")
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>
) : (
collection.items.map((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>
))
)}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.RootProvider>
)
}
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
自定义项
使用您自己的组件自定义下拉菜单中项目的显示。





























"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",
},
]
自定义过滤器
这是一个自定义过滤器的示例,它可以匹配项目的多个属性。
"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",
},
]
自定义动画
要自定义组合框的动画,请将 _open
和 _closed
属性传递给 Combobox.Content
组件。
"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" },
]
弹出框
要在弹出框组件中使用组合框,请避免将 Combobox.Positioner
包装在 Portal
中。
-<Portal>
<Combobox.Positioner>
<Combobox.Content>
{/* ... */}
</Combobox.Content>
</Combobox.Positioner>
-</Portal>
"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" },
]
属性
Root
属性名 | 默认值 | 类型 |
---|---|---|
collection * | ListCollection<T> 项目集合 | |
composite | true | boolean 组合框是否与其他复合小部件(如选项卡)组合 |
inputBehavior | '\'none\'' | 'none' | 'autohighlight' | 'autocomplete' 定义组合框的自动完成行为。 - `autohighlight`: 用户输入时,第一个焦点项会被高亮显示 - `autocomplete`: 使用方向键导航列表框时会选择项目并更新输入框 |
lazyMount | false | boolean 是否启用懒加载 |
loopFocus | true | boolean 键盘导航是否在项目间循环 |
openOnChange | true | boolean | ((details: InputValueChangeDetails) => boolean) 输入值改变时是否显示组合框 |
openOnClick | false | boolean 首次点击输入框时是否打开组合框弹出窗口 |
openOnKeyPress | true | boolean 按下方向键时是否打开组合框 |
selectionBehavior | '\'replace\'' | 'replace' | 'clear' | 'preserve' 选择项目时组合框输入框的行为 - `replace`: 选定的项目字符串被设置为输入值 - `clear`: 输入值被清除 - `preserve`: 输入值被保留 |
unmountOnExit | false | boolean 是否在退出时卸载。 |
allowCustomValue | boolean 是否允许在输入框中输入自定义值 | |
asChild | ||
autoFocus | boolean 是否在挂载时自动聚焦输入框 | |
closeOnSelect | boolean 选择项目时是否关闭组合框。 | |
defaultOpen | boolean 组合框首次渲染时的初始打开状态。在您不需要控制其打开状态时使用。 | |
defaultValue | string[] 组合框首次渲染时的初始值。在您不需要控制组合框状态时使用。 | |
disabled | boolean 组合框是否禁用 | |
disableLayer | boolean 是否禁用将此注册为可关闭层 | |
form | string 组合框的关联表单。 | |
highlightedValue | string 活动项目的 ID。用于设置 `aria-activedescendant` 属性 | |
id | string 机器的唯一标识符。 | |
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 }> 组合框中元素的 ID。对组合很有用。 | |
immediate | boolean 是立即同步当前更改还是将其推迟到下一帧 | |
inputValue | string 组合框输入框的当前值 | |
invalid | boolean 组合框是否无效 | |
multiple | boolean 是否允许多选。**请注意:**当 `multiple` 为 `true` 时,`selectionBehavior` 会自动设置为 `clear`。建议在单独的容器中渲染已选择的项目。 | |
name | string 组合框输入框的 `name` 属性。对表单提交很有用。 | |
navigate | (details: NavigateDetails) => void 导航到所选项目的功能 | |
onExitComplete | () => void 当动画在关闭状态下结束时调用的函数 | |
onFocusOutside | (event: FocusOutsideEvent) => void 当焦点移出组件时调用的函数 | |
onHighlightChange | (details: HighlightChangeDetails<T>) => void 使用指针或键盘导航高亮显示项目时调用的函数。 | |
onInputValueChange | (details: InputValueChangeDetails) => void 当输入框的值改变时调用的函数 | |
onInteractOutside | (event: InteractOutsideEvent) => void 当组件外部发生交互时调用的函数 | |
onOpenChange | (details: OpenChangeDetails) => void 弹出窗口打开时调用的函数 | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void 当指针在组件外部按下时调用的函数 | |
onValueChange | (details: ValueChangeDetails<T>) => void 选择新项目时调用的函数 | |
open | boolean 组合框是否打开 | |
placeholder | string 组合框输入框的占位符文本 | |
positioning | PositioningOptions 动态定位菜单的定位选项 | |
present | boolean 节点是否存在(由用户控制) | |
readOnly | boolean 组合框是否只读。这将使组合框处于“不可编辑”模式,但用户仍然可以与其交互 | |
required | boolean 组合框是否为必填项 | |
scrollToIndexFn | (details: ScrollToIndexDetails) => void 滚动到特定索引的函数 | |
translations | IntlTranslations 指定识别可访问性元素及其状态的本地化字符串 | |
value | string[] 所选项目的键 |
Item
属性名 | 默认值 | 类型 |
---|---|---|
asChild | ||
item | any 要渲染的项目 | |
persistFocus | boolean 悬停在外部时是否清除高亮状态 |