405 lines
9.8 KiB
TypeScript
405 lines
9.8 KiB
TypeScript
"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,
|
|
}
|