snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
404
apps/v4/registry/bases/aria/ui/combobox.tsx
Normal file
404
apps/v4/registry/bases/aria/ui/combobox.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "cn"
|
||||
import {
|
||||
Button as ButtonPrimitive,
|
||||
Collection,
|
||||
ComboBox as ComboBoxPrimitive,
|
||||
ComboBoxStateContext,
|
||||
ComboBoxValue as ComboBoxValuePrimitive,
|
||||
composeRenderProps,
|
||||
Group,
|
||||
Header as HeaderPrimitive,
|
||||
Input as InputPrimitive,
|
||||
ListBoxItem as ListBoxItemPrimitive,
|
||||
ListBox as ListBoxPrimitive,
|
||||
ListBoxSection as ListBoxSectionPrimitive,
|
||||
Popover as PopoverPrimitive,
|
||||
Separator as SeparatorPrimitive,
|
||||
TagGroup as TagGroupPrimitive,
|
||||
TagList as TagListPrimitive,
|
||||
Tag as TagPrimitive,
|
||||
type ButtonProps,
|
||||
type ComboBoxValueProps,
|
||||
type GroupProps,
|
||||
type HeaderProps,
|
||||
type InputProps,
|
||||
type ListBoxItemProps,
|
||||
type ListBoxProps,
|
||||
type ListBoxSectionProps,
|
||||
type SeparatorProps,
|
||||
type TagListProps,
|
||||
type TagProps,
|
||||
} from "react-aria-components"
|
||||
|
||||
import { Button } from "@/registry/bases/aria/ui/button"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/aria/ui/input-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
function ComboboxValue<T>({ ...props }: ComboBoxValueProps<T>) {
|
||||
return <ComboBoxValuePrimitive data-slot="combobox-value" {...props} />
|
||||
}
|
||||
|
||||
function ComboboxTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: Omit<ButtonProps, "children"> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="combobox-trigger"
|
||||
className={cn("cn-combobox-trigger", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconPlaceholder
|
||||
lucide="ChevronDownIcon"
|
||||
tabler="IconChevronDown"
|
||||
hugeicons="ArrowDown01Icon"
|
||||
phosphor="CaretDownIcon"
|
||||
remixicon="RiArrowDownSLine"
|
||||
className="cn-combobox-trigger-icon pointer-events-none"
|
||||
/>
|
||||
</ButtonPrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxClear({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof InputGroupButton>) {
|
||||
const state = React.useContext(ComboBoxStateContext)
|
||||
if (state?.inputValue === "") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroupButton
|
||||
data-slot="combobox-clear"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label="Clear"
|
||||
className={cn("cn-combobox-clear", className)}
|
||||
onPress={() => {
|
||||
state?.setValue(null)
|
||||
}}
|
||||
slot={null}
|
||||
{...props}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
className="cn-combobox-clear-icon pointer-events-none"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxInput({
|
||||
className,
|
||||
children,
|
||||
disabled = false,
|
||||
showTrigger = true,
|
||||
showClear = false,
|
||||
...props
|
||||
}: React.ComponentProps<"input"> & {
|
||||
showTrigger?: boolean
|
||||
showClear?: boolean
|
||||
}) {
|
||||
return (
|
||||
<InputGroup className={cn("cn-combobox-input w-auto", className)}>
|
||||
<InputGroupInput disabled={disabled} {...props} />
|
||||
<InputGroupAddon align="inline-end">
|
||||
{showTrigger && (
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
data-slot="combobox-trigger"
|
||||
className="cn-combobox-trigger group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
||||
isDisabled={disabled}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronDownIcon"
|
||||
tabler="IconChevronDown"
|
||||
hugeicons="ArrowDown01Icon"
|
||||
phosphor="CaretDownIcon"
|
||||
remixicon="RiArrowDownSLine"
|
||||
className="cn-combobox-trigger-icon pointer-events-none"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
)}
|
||||
{showClear && <ComboboxClear isDisabled={disabled} />}
|
||||
</InputGroupAddon>
|
||||
{children}
|
||||
</InputGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxContent({
|
||||
className,
|
||||
placement = "bottom",
|
||||
offset = 6,
|
||||
crossOffset = 0,
|
||||
anchor,
|
||||
...props
|
||||
}: Omit<
|
||||
React.ComponentProps<typeof PopoverPrimitive>,
|
||||
"className" | "children"
|
||||
> & {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
anchor?: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
return (
|
||||
<PopoverPrimitive
|
||||
data-slot="combobox-content"
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
crossOffset={crossOffset}
|
||||
triggerRef={anchor}
|
||||
className={cn(
|
||||
"cn-combobox-content-aria cn-menu-target cn-menu-translucent cn-menu-translucent-aria relative isolate z-50 w-(--trigger-width) origin-(--trigger-anchor-point)",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxList<T extends object>({
|
||||
className,
|
||||
...props
|
||||
}: ListBoxProps<T>) {
|
||||
return (
|
||||
<ListBoxPrimitive
|
||||
data-slot="combobox-list"
|
||||
className={cn(
|
||||
"cn-combobox-list group/combobox-content max-h-[inherit] overflow-y-auto overscroll-contain",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxItem<T extends object>({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ListBoxItemProps<T>) {
|
||||
return (
|
||||
<ListBoxItemPrimitive
|
||||
data-slot="combobox-item"
|
||||
textValue={typeof children === "string" ? children : undefined}
|
||||
className={cn(
|
||||
"cn-combobox-item cn-combobox-item-aria relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{composeRenderProps(children, (children, { isSelected }) => (
|
||||
<>
|
||||
{children}
|
||||
<span className="cn-combobox-item-indicator">
|
||||
{isSelected ? (
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
className="cn-combobox-item-indicator-icon pointer-events-none"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</>
|
||||
))}
|
||||
</ListBoxItemPrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxGroup<T extends object>({
|
||||
className,
|
||||
...props
|
||||
}: ListBoxSectionProps<T>) {
|
||||
return (
|
||||
<ListBoxSectionPrimitive
|
||||
data-slot="combobox-group"
|
||||
className={cn("cn-combobox-group", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxLabel({ className, ...props }: HeaderProps) {
|
||||
return (
|
||||
<HeaderPrimitive
|
||||
data-slot="combobox-label"
|
||||
className={cn("cn-combobox-label", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxEmpty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="combobox-empty"
|
||||
className={cn("cn-combobox-empty", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxSeparator({ className, ...props }: SeparatorProps) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="combobox-separator"
|
||||
className={cn("cn-combobox-separator", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChips({ children, className, ...props }: GroupProps) {
|
||||
return (
|
||||
<Group
|
||||
data-slot="combobox-chips"
|
||||
className={cn("cn-combobox-chips", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChipList<T extends object>({
|
||||
className,
|
||||
...props
|
||||
}: Omit<TagListProps<T>, "className" | "items"> & {
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<ComboBoxValuePrimitive<T> className="contents">
|
||||
{({ selectedItems, state }) => (
|
||||
<TagGroupPrimitive
|
||||
data-slot="combobox-chip-list"
|
||||
className={cn("contents", className)}
|
||||
onRemove={(keys) => {
|
||||
if (Array.isArray(state.value)) {
|
||||
state.setValue(state.value.filter((k) => !keys.has(k)))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TagListPrimitive
|
||||
className="contents"
|
||||
items={selectedItems.filter((item) => item != null)}
|
||||
{...props}
|
||||
/>
|
||||
</TagGroupPrimitive>
|
||||
)}
|
||||
</ComboBoxValuePrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChip({
|
||||
className,
|
||||
children,
|
||||
showRemove = true,
|
||||
...props
|
||||
}: Omit<TagProps, "children"> & {
|
||||
showRemove?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<TagPrimitive
|
||||
data-slot="combobox-chip"
|
||||
className={cn(
|
||||
"cn-combobox-chip has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showRemove && (
|
||||
<Button
|
||||
slot="remove"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="cn-combobox-chip-remove"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
className="cn-combobox-chip-indicator-icon pointer-events-none"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</TagPrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxChipsInput({ className, ...props }: InputProps) {
|
||||
const state = React.useContext(ComboBoxStateContext)
|
||||
return (
|
||||
<InputPrimitive
|
||||
data-slot="combobox-chip-input"
|
||||
className={cn(
|
||||
"cn-combobox-chip-input min-w-16 flex-1 outline-none",
|
||||
className
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === "Backspace" &&
|
||||
e.currentTarget.value === "" &&
|
||||
Array.isArray(state?.value) &&
|
||||
state.value.length > 0
|
||||
) {
|
||||
e.preventDefault()
|
||||
state.setValue(state.value.slice(0, -1))
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function useComboboxAnchor() {
|
||||
return React.useRef<HTMLDivElement | null>(null)
|
||||
}
|
||||
|
||||
export {
|
||||
ComboBoxPrimitive as Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxItem,
|
||||
ComboboxGroup,
|
||||
ComboboxLabel,
|
||||
Collection as ComboboxCollection,
|
||||
ComboboxEmpty,
|
||||
ComboboxSeparator,
|
||||
ComboboxChips,
|
||||
ComboboxChip,
|
||||
ComboboxChipList,
|
||||
ComboboxChipsInput,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
}
|
||||
Reference in New Issue
Block a user