135 lines
4.3 KiB
TypeScript
135 lines
4.3 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { cn } from "cn"
|
|
import { CheckIcon, ChevronDownIcon } from "lucide-react"
|
|
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "./command"
|
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
|
import { useConnectedPopupRef } from "./use-connected-popup-ref"
|
|
|
|
// A searchable single select composed from the public Popover and Command (not Select, not a copy of its list):
|
|
// <Popover><PopoverTrigger = field-like button role="combobox"/><PopoverContent><Command>Input + List</Command></PopoverContent></Popover>
|
|
// Select stays for short lists that need no search (Radix Select). Combobox is for search / filter. Filtering, keyboard and the
|
|
// listbox ARIA are cmdk's; Popover keeps Portal, collision, Escape and focus return to the trigger.
|
|
// The typed `options` wrapper is a minimal API (value, label, disabled); anything richer is composed with Command directly.
|
|
type ComboboxOption = {
|
|
value: string
|
|
label: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
type ComboboxSize = "medium" | "large"
|
|
|
|
type ComboboxProps = Omit<
|
|
React.ComponentProps<"button">,
|
|
"value" | "defaultValue" | "onChange" | "children"
|
|
> & {
|
|
options: ComboboxOption[]
|
|
value?: string
|
|
defaultValue?: string
|
|
/** Explicit value ownership. Omitted: infer from value !== undefined. Keep this mode stable while mounted. */
|
|
controlled?: boolean
|
|
onValueChange?: (value: string) => void
|
|
placeholder?: string
|
|
searchPlaceholder?: string
|
|
emptyText?: string
|
|
size?: ComboboxSize
|
|
}
|
|
|
|
function Combobox({
|
|
className,
|
|
options,
|
|
value: valueProp,
|
|
defaultValue,
|
|
controlled: controlledProp,
|
|
onValueChange,
|
|
placeholder = "선택",
|
|
searchPlaceholder = "검색...",
|
|
emptyText = "결과가 없습니다.",
|
|
size = "large",
|
|
disabled,
|
|
...props
|
|
}: ComboboxProps) {
|
|
const [open, setOpen] = React.useState(false)
|
|
const [inner, setInner] = React.useState<string | undefined>(defaultValue)
|
|
const controlled = controlledProp ?? (valueProp !== undefined)
|
|
const value = controlled ? valueProp : inner
|
|
const selected = options.find((option) => option.value === value)
|
|
const popupRef = useConnectedPopupRef()
|
|
|
|
const select = (next: string) => {
|
|
if (!controlled) setInner(next)
|
|
onValueChange?.(next)
|
|
setOpen(false)
|
|
}
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
role="combobox"
|
|
aria-haspopup="listbox"
|
|
aria-expanded={open}
|
|
data-slot="combobox-trigger"
|
|
data-size={size}
|
|
data-empty={selected ? undefined : ""}
|
|
disabled={disabled}
|
|
className={cn("flex w-full items-center justify-between text-left outline-none disabled:cursor-not-allowed", className)}
|
|
{...props}
|
|
>
|
|
<span data-slot="combobox-value">{selected ? selected.label : placeholder}</span>
|
|
<ChevronDownIcon data-slot="combobox-icon" aria-hidden="true" />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
ref={popupRef}
|
|
data-combobox-popup=""
|
|
data-size={size}
|
|
align="start"
|
|
sideOffset={-1}
|
|
style={
|
|
{
|
|
"--hm-popover-padding": "0px",
|
|
"--hm-popover-width": "var(--radix-popover-trigger-width)",
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder={searchPlaceholder} />
|
|
<CommandList>
|
|
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
{options.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
value={option.value}
|
|
keywords={[option.label]}
|
|
disabled={option.disabled}
|
|
data-checked={option.value === value ? "" : undefined}
|
|
onSelect={() => select(option.value)}
|
|
>
|
|
<span data-slot="combobox-item-label">{option.label}</span>
|
|
<CheckIcon
|
|
data-slot="combobox-item-check"
|
|
aria-hidden="true"
|
|
className={option.value === value ? "opacity-100" : "opacity-0"}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
|
|
export { Combobox }
|
|
export type { ComboboxOption, ComboboxSize }
|