Add a combobox component with auto-complete functionality

This commit is contained in:
System233 2025-02-26 02:44:57 +08:00
parent 82b282ba01
commit d5b796263d
4 changed files with 862 additions and 0 deletions

View file

@ -0,0 +1,6 @@
import React from "react"
export const Check = () => React.createElement("div")
export const ChevronsUpDown = () => React.createElement("div")
export const Loader = () => React.createElement("div")
export const X = () => React.createElement("div")

View file

@ -0,0 +1,522 @@
/* eslint-disable react/jsx-pascal-case */
"use client"
import * as React from "react"
import { composeEventHandlers } from "@radix-ui/primitive"
import { useComposedRefs } from "@radix-ui/react-compose-refs"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { Primitive } from "@radix-ui/react-primitive"
import * as RovingFocusGroupPrimitive from "@radix-ui/react-roving-focus"
import { useControllableState } from "@radix-ui/react-use-controllable-state"
import { Command as CommandPrimitive } from "cmdk"
export type ComboboxContextProps = {
inputValue: string
onInputValueChange: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
onInputBlur?: (e: React.FocusEvent<HTMLInputElement, Element>) => void
open: boolean
onOpenChange: (open: boolean) => void
currentTabStopId: string | null
onCurrentTabStopIdChange: (currentTabStopId: string | null) => void
inputRef: React.RefObject<HTMLInputElement>
tagGroupRef: React.RefObject<React.ElementRef<typeof RovingFocusGroupPrimitive.Root>>
disabled?: boolean
required?: boolean
} & (
| Required<Pick<ComboboxSingleProps, "type" | "value" | "onValueChange">>
| Required<Pick<ComboboxMultipleProps, "type" | "value" | "onValueChange">>
)
const ComboboxContext = React.createContext<ComboboxContextProps>({
type: "single",
value: "",
onValueChange: () => {},
inputValue: "",
onInputValueChange: () => {},
onInputBlur: () => {},
open: false,
onOpenChange: () => {},
currentTabStopId: null,
onCurrentTabStopIdChange: () => {},
inputRef: { current: null },
tagGroupRef: { current: null },
disabled: false,
required: false,
})
export const useComboboxContext = () => React.useContext(ComboboxContext)
export type ComboboxType = "single" | "multiple"
export interface ComboboxBaseProps
extends React.ComponentProps<typeof PopoverPrimitive.Root>,
Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange"> {
type?: ComboboxType | undefined
inputValue?: string
defaultInputValue?: string
onInputValueChange?: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
onInputBlur?: (e: React.FocusEvent<HTMLInputElement, Element>) => void
disabled?: boolean
required?: boolean
}
export type ComboboxValue<T extends ComboboxType = "single"> = T extends "single"
? string
: T extends "multiple"
? string[]
: never
export interface ComboboxSingleProps {
type: "single"
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
}
export interface ComboboxMultipleProps {
type: "multiple"
value?: string[]
defaultValue?: string[]
onValueChange?: (value: string[]) => void
}
export type ComboboxProps = ComboboxBaseProps & (ComboboxSingleProps | ComboboxMultipleProps)
export const Combobox = React.forwardRef(
<T extends ComboboxType = "single">(
{
type = "single" as T,
open: openProp,
onOpenChange,
defaultOpen,
modal,
children,
value: valueProp,
defaultValue,
onValueChange,
inputValue: inputValueProp,
defaultInputValue,
onInputValueChange,
onInputBlur,
disabled,
required,
...props
}: ComboboxProps,
ref: React.ForwardedRef<React.ElementRef<typeof CommandPrimitive>>,
) => {
const [value = type === "multiple" ? [] : "", setValue] = useControllableState<ComboboxValue<T>>({
prop: valueProp as ComboboxValue<T>,
defaultProp: defaultValue as ComboboxValue<T>,
onChange: onValueChange as (value: ComboboxValue<T>) => void,
})
const [inputValue = "", setInputValue] = useControllableState({
prop: inputValueProp,
defaultProp: defaultInputValue,
})
const [open = false, setOpen] = useControllableState({
prop: openProp,
defaultProp: defaultOpen,
onChange: onOpenChange,
})
const [currentTabStopId, setCurrentTabStopId] = React.useState<string | null>(null)
const inputRef = React.useRef<HTMLInputElement>(null)
const tagGroupRef = React.useRef<React.ElementRef<typeof RovingFocusGroupPrimitive.Root>>(null)
const handleInputValueChange: ComboboxContextProps["onInputValueChange"] = React.useCallback(
(inputValue, reason) => {
setInputValue(inputValue)
onInputValueChange?.(inputValue, reason)
},
[setInputValue, onInputValueChange],
)
return (
<ComboboxContext.Provider
value={
{
type,
value,
onValueChange: setValue,
inputValue,
onInputValueChange: handleInputValueChange,
onInputBlur,
open,
onOpenChange: setOpen,
currentTabStopId,
onCurrentTabStopIdChange: setCurrentTabStopId,
inputRef,
tagGroupRef,
disabled,
required,
} as ComboboxContextProps
}>
<PopoverPrimitive.Root open={open} onOpenChange={setOpen} modal={modal}>
<CommandPrimitive ref={ref} {...props}>
{children}
{!open && <CommandPrimitive.List aria-hidden hidden />}
</CommandPrimitive>
</PopoverPrimitive.Root>
</ComboboxContext.Provider>
)
},
)
Combobox.displayName = "Combobox"
export const ComboboxTagGroup = React.forwardRef<
React.ElementRef<typeof RovingFocusGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RovingFocusGroupPrimitive.Root>
>((props, ref) => {
const { currentTabStopId, onCurrentTabStopIdChange, tagGroupRef, type } = useComboboxContext()
if (type !== "multiple") {
throw new Error('<ComboboxTagGroup> should only be used when type is "multiple"')
}
const composedRefs = useComposedRefs(ref, tagGroupRef)
return (
<RovingFocusGroupPrimitive.Root
ref={composedRefs}
tabIndex={-1}
currentTabStopId={currentTabStopId}
onCurrentTabStopIdChange={onCurrentTabStopIdChange}
onBlur={() => onCurrentTabStopIdChange(null)}
{...props}
/>
)
})
ComboboxTagGroup.displayName = "ComboboxTagGroup"
export interface ComboboxTagGroupItemProps
extends React.ComponentPropsWithoutRef<typeof RovingFocusGroupPrimitive.Item> {
value: string
disabled?: boolean
}
const ComboboxTagGroupItemContext = React.createContext<Pick<ComboboxTagGroupItemProps, "value" | "disabled">>({
value: "",
disabled: false,
})
const useComboboxTagGroupItemContext = () => React.useContext(ComboboxTagGroupItemContext)
export const ComboboxTagGroupItem = React.forwardRef<
React.ElementRef<typeof RovingFocusGroupPrimitive.Item>,
ComboboxTagGroupItemProps
>(({ onClick, onKeyDown, value: valueProp, disabled, ...props }, ref) => {
const { value, onValueChange, inputRef, currentTabStopId, type } = useComboboxContext()
if (type !== "multiple") {
throw new Error('<ComboboxTagGroupItem> should only be used when type is "multiple"')
}
const lastItemValue = value.at(-1)
return (
<ComboboxTagGroupItemContext.Provider value={{ value: valueProp, disabled }}>
<RovingFocusGroupPrimitive.Item
ref={ref}
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
if (event.key === "Escape") {
inputRef.current?.focus()
}
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault()
inputRef.current?.focus()
}
if (event.key === "ArrowRight" && currentTabStopId === lastItemValue) {
inputRef.current?.focus()
}
if (event.key === "Backspace" || event.key === "Delete") {
onValueChange(value.filter((v) => v !== currentTabStopId))
inputRef.current?.focus()
}
})}
onClick={composeEventHandlers(onClick, () => disabled && inputRef.current?.focus())}
tabStopId={valueProp}
focusable={!disabled}
data-disabled={disabled}
active={valueProp === lastItemValue}
{...props}
/>
</ComboboxTagGroupItemContext.Provider>
)
})
ComboboxTagGroupItem.displayName = "ComboboxTagGroupItem"
export const ComboboxTagGroupItemRemove = React.forwardRef<
React.ElementRef<typeof Primitive.button>,
React.ComponentPropsWithoutRef<typeof Primitive.button>
>(({ onClick, ...props }, ref) => {
const { value, onValueChange, type } = useComboboxContext()
if (type !== "multiple") {
throw new Error('<ComboboxTagGroupItemRemove> should only be used when type is "multiple"')
}
const { value: valueProp, disabled } = useComboboxTagGroupItemContext()
return (
<Primitive.button
ref={ref}
aria-hidden
tabIndex={-1}
disabled={disabled}
onClick={composeEventHandlers(onClick, () => onValueChange(value.filter((v) => v !== valueProp)))}
{...props}
/>
)
})
ComboboxTagGroupItemRemove.displayName = "ComboboxTagGroupItemRemove"
export const ComboboxInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
Omit<React.ComponentProps<typeof CommandPrimitive.Input>, "value" | "onValueChange">
>(({ onKeyDown, onMouseDown, onFocus, onBlur, ...props }, ref) => {
const {
type,
inputValue,
onInputValueChange,
onInputBlur,
open,
onOpenChange,
value,
onValueChange,
inputRef,
disabled,
required,
tagGroupRef,
} = useComboboxContext()
const composedRefs = useComposedRefs(ref, inputRef)
return (
<CommandPrimitive.Input
ref={composedRefs}
disabled={disabled}
required={required}
value={inputValue}
onValueChange={(search) => {
if (!open) {
onOpenChange(true)
}
// Schedule input value change to the next tick.
setTimeout(() => onInputValueChange(search, "inputChange"))
if (!search && type === "single") {
onValueChange("")
}
}}
onKeyDown={composeEventHandlers(onKeyDown, (event) => {
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
if (!open) {
event.preventDefault()
onOpenChange(true)
}
}
if (type !== "multiple") {
return
}
if (event.key === "ArrowLeft" && !inputValue && value.length) {
tagGroupRef.current?.focus()
}
if (event.key === "Backspace" && !inputValue) {
onValueChange(value.slice(0, -1))
}
})}
onMouseDown={composeEventHandlers(onMouseDown, () => onOpenChange(!!inputValue || !open))}
onFocus={composeEventHandlers(onFocus, () => onOpenChange(true))}
onBlur={composeEventHandlers(onBlur, (event) => {
if (!event.relatedTarget?.hasAttribute("cmdk-list")) {
onInputBlur?.(event)
}
})}
{...props}
/>
)
})
ComboboxInput.displayName = "ComboboxInput"
export const ComboboxClear = React.forwardRef<
React.ElementRef<typeof Primitive.button>,
React.ComponentPropsWithoutRef<typeof Primitive.button>
>(({ onClick, ...props }, ref) => {
const { value, onValueChange, inputValue, onInputValueChange, type } = useComboboxContext()
const isValueEmpty = type === "single" ? !value : !value.length
return (
<Primitive.button
ref={ref}
disabled={isValueEmpty && !inputValue}
onClick={composeEventHandlers(onClick, () => {
if (type === "single") {
onValueChange("")
} else {
onValueChange([])
}
onInputValueChange("", "clearClick")
})}
{...props}
/>
)
})
ComboboxClear.displayName = "ComboboxClear"
export const ComboboxTrigger = PopoverPrimitive.Trigger
export const ComboboxAnchor = PopoverPrimitive.Anchor
export const ComboboxPortal = PopoverPrimitive.Portal
export const ComboboxContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ children, onOpenAutoFocus, onInteractOutside, ...props }, ref) => (
<PopoverPrimitive.Content
asChild
ref={ref}
onOpenAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())}
onCloseAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())}
onInteractOutside={composeEventHandlers(onInteractOutside, (event) => {
if (event.target instanceof Element && event.target.hasAttribute("cmdk-input")) {
event.preventDefault()
}
})}
{...props}>
<CommandPrimitive.List>{children}</CommandPrimitive.List>
</PopoverPrimitive.Content>
))
ComboboxContent.displayName = "ComboboxContent"
export const ComboboxEmpty = CommandPrimitive.Empty
export const ComboboxLoading = CommandPrimitive.Loading
export interface ComboboxItemProps extends Omit<React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>, "value"> {
value: string
}
const ComboboxItemContext = React.createContext({ isSelected: false })
const useComboboxItemContext = () => React.useContext(ComboboxItemContext)
const findComboboxItemText = (children: React.ReactNode) => {
let text = ""
React.Children.forEach(children, (child) => {
if (text) {
return
}
if (React.isValidElement<{ children: React.ReactNode }>(child)) {
if (child.type === ComboboxItemText) {
text = child.props.children as string
} else {
text = findComboboxItemText(child.props.children)
}
}
})
return text
}
export const ComboboxItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, ComboboxItemProps>(
({ value: valueProp, children, onMouseDown, ...props }, ref) => {
const { type, value, onValueChange, onInputValueChange, onOpenChange } = useComboboxContext()
const inputValue = React.useMemo(() => findComboboxItemText(children), [children])
const isSelected = type === "single" ? value === valueProp : value.includes(valueProp)
return (
<ComboboxItemContext.Provider value={{ isSelected }}>
<CommandPrimitive.Item
ref={ref}
onMouseDown={composeEventHandlers(onMouseDown, (event) => event.preventDefault())}
onSelect={() => {
if (type === "multiple") {
onValueChange(
value.includes(valueProp)
? value.filter((v) => v !== valueProp)
: [...value, valueProp],
)
onInputValueChange("", "itemSelect")
} else {
onValueChange(valueProp)
onInputValueChange(inputValue, "itemSelect")
// Schedule open change to the next tick.
setTimeout(() => onOpenChange(false))
}
}}
value={inputValue}
{...props}>
{children}
</CommandPrimitive.Item>
</ComboboxItemContext.Provider>
)
},
)
ComboboxItem.displayName = "ComboboxItem"
export const ComboboxItemIndicator = React.forwardRef<
React.ElementRef<typeof Primitive.span>,
React.ComponentPropsWithoutRef<typeof Primitive.span>
>((props, ref) => {
const { isSelected } = useComboboxItemContext()
if (!isSelected) {
return null
}
return <Primitive.span ref={ref} aria-hidden {...props} />
})
ComboboxItemIndicator.displayName = "ComboboxItemIndicator"
export interface ComboboxItemTextProps extends React.ComponentPropsWithoutRef<typeof React.Fragment> {
children: string
}
export const ComboboxItemText = (props: ComboboxItemTextProps) => <React.Fragment {...props} />
ComboboxItemText.displayName = "ComboboxItemText"
export const ComboboxGroup = CommandPrimitive.Group
export const ComboboxSeparator = CommandPrimitive.Separator
const Root = Combobox
const TagGroup = ComboboxTagGroup
const TagGroupItem = ComboboxTagGroupItem
const TagGroupItemRemove = ComboboxTagGroupItemRemove
const Input = ComboboxInput
const Clear = ComboboxClear
const Trigger = ComboboxTrigger
const Anchor = ComboboxAnchor
const Portal = ComboboxPortal
const Content = ComboboxContent
const Empty = ComboboxEmpty
const Loading = ComboboxLoading
const Item = ComboboxItem
const ItemIndicator = ComboboxItemIndicator
const ItemText = ComboboxItemText
const Group = ComboboxGroup
const Separator = ComboboxSeparator
export {
Root,
TagGroup,
TagGroupItem,
TagGroupItemRemove,
Input,
Clear,
Trigger,
Anchor,
Portal,
Content,
Empty,
Loading,
Item,
ItemIndicator,
ItemText,
Group,
Separator,
}

View file

@ -0,0 +1,177 @@
"use client"
import * as React from "react"
import { Slottable } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority"
import { Check, ChevronsUpDown, Loader, X } from "lucide-react"
import { cn } from "@/lib/utils"
import * as ComboboxPrimitive from "@/components/ui/combobox-primitive"
import { badgeVariants } from "@/components/ui/badge"
// import * as ComboboxPrimitive from "@/registry/default/ui/combobox-primitive"
import {
InputBase,
InputBaseAdornmentButton,
InputBaseControl,
InputBaseFlexWrapper,
InputBaseInput,
} from "@/components/ui/input-base"
export const Combobox = ComboboxPrimitive.Root
const ComboboxInputBase = React.forwardRef<
React.ElementRef<typeof InputBase>,
React.ComponentPropsWithoutRef<typeof InputBase>
>(({ children, ...props }, ref) => (
<ComboboxPrimitive.Anchor asChild>
<InputBase ref={ref} {...props}>
{children}
<ComboboxPrimitive.Clear asChild>
<InputBaseAdornmentButton>
<X />
</InputBaseAdornmentButton>
</ComboboxPrimitive.Clear>
<ComboboxPrimitive.Trigger asChild>
<InputBaseAdornmentButton>
<ChevronsUpDown />
</InputBaseAdornmentButton>
</ComboboxPrimitive.Trigger>
</InputBase>
</ComboboxPrimitive.Anchor>
))
ComboboxInputBase.displayName = "ComboboxInputBase"
export const ComboboxInput = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Input>
>((props, ref) => (
<ComboboxInputBase>
<InputBaseControl>
<ComboboxPrimitive.Input asChild>
<InputBaseInput ref={ref} {...props} />
</ComboboxPrimitive.Input>
</InputBaseControl>
</ComboboxInputBase>
))
ComboboxInput.displayName = "ComboboxInput"
export const ComboboxTagsInput = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Input>
>(({ children, ...props }, ref) => (
<ComboboxInputBase>
<ComboboxPrimitive.ComboboxTagGroup asChild>
<InputBaseFlexWrapper className="flex items-center gap-2">
{children}
<InputBaseControl>
<ComboboxPrimitive.Input asChild>
<InputBaseInput ref={ref} {...props} />
</ComboboxPrimitive.Input>
</InputBaseControl>
</InputBaseFlexWrapper>
</ComboboxPrimitive.ComboboxTagGroup>
</ComboboxInputBase>
))
ComboboxTagsInput.displayName = "ComboboxTagsInput"
export const ComboboxTag = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.ComboboxTagGroupItem>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.ComboboxTagGroupItem>
>(({ children, className, ...props }, ref) => (
<ComboboxPrimitive.ComboboxTagGroupItem
ref={ref}
className={cn(
badgeVariants({ variant: "outline" }),
"group gap-1 pr-1.5 data-[disabled]:opacity-50",
className,
)}
{...props}>
<Slottable>{children}</Slottable>
<ComboboxPrimitive.ComboboxTagGroupItemRemove className="group-data-[disabled]:pointer-events-none">
<X className="size-4" />
<span className="sr-only">Remove</span>
</ComboboxPrimitive.ComboboxTagGroupItemRemove>
</ComboboxPrimitive.ComboboxTagGroupItem>
))
ComboboxTag.displayName = "ComboboxTag"
export const ComboboxContent = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Content>
>(({ className, align = "start", alignOffset = 0, ...props }, ref) => (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
className={cn(
"min-w-72 border-vscode-dropdown-border relative z-50 left-0 max-h-96 w-[--radix-popover-trigger-width] overflow-y-auto overflow-x-hidden rounded-xs border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ComboboxPrimitive.Portal>
))
ComboboxContent.displayName = "ComboboxContent"
export const ComboboxEmpty = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Empty>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Empty ref={ref} className={cn("py-6 text-center text-sm", className)} {...props} />
))
ComboboxEmpty.displayName = "ComboboxEmpty"
export const ComboboxLoading = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Loading>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Loading>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Loading
ref={ref}
className={cn("flex items-center justify-center px-1.5 py-2", className)}
{...props}>
<Loader className="size-4 animate-spin [mask:conic-gradient(transparent_45deg,_white)]" />
</ComboboxPrimitive.Loading>
))
ComboboxLoading.displayName = "ComboboxLoading"
export const ComboboxGroup = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Group>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Group
ref={ref}
className={cn(
"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-semibold",
className,
)}
{...props}
/>
))
ComboboxGroup.displayName = "ComboboxGroup"
const ComboboxSeparator = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
))
ComboboxSeparator.displayName = "ComboboxSeparator"
export const comboboxItemStyle = cva(
"relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-vscode-dropdown-foreground data-[disabled=true]:opacity-50",
)
export const ComboboxItem = React.forwardRef<
React.ElementRef<typeof ComboboxPrimitive.Item>,
Omit<React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Item>, "children"> &
Pick<React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.ItemText>, "children">
>(({ className, children, ...props }, ref) => (
<ComboboxPrimitive.Item ref={ref} className={cn(comboboxItemStyle(), className)} {...props}>
<ComboboxPrimitive.ItemText>{children}</ComboboxPrimitive.ItemText>
<ComboboxPrimitive.ItemIndicator className="absolute right-2 flex size-3.5 items-center justify-center">
<Check className="size-4" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
))
ComboboxItem.displayName = "ComboboxItem"

View file

@ -0,0 +1,157 @@
/* eslint-disable react/jsx-no-comment-textnodes */
/* eslint-disable react/jsx-pascal-case */
"use client"
import * as React from "react"
import { composeEventHandlers } from "@radix-ui/primitive"
import { composeRefs } from "@radix-ui/react-compose-refs"
import { Primitive } from "@radix-ui/react-primitive"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { Button } from "./button"
export type InputBaseContextProps = Pick<InputBaseProps, "autoFocus" | "disabled"> & {
controlRef: React.RefObject<HTMLElement>
onFocusedChange: (focused: boolean) => void
}
const InputBaseContext = React.createContext<InputBaseContextProps>({
autoFocus: false,
controlRef: { current: null },
disabled: false,
onFocusedChange: () => {},
})
const useInputBaseContext = () => React.useContext(InputBaseContext)
export interface InputBaseProps extends React.ComponentPropsWithoutRef<typeof Primitive.div> {
autoFocus?: boolean
disabled?: boolean
}
export const InputBase = React.forwardRef<React.ElementRef<typeof Primitive.div>, InputBaseProps>(
({ autoFocus, disabled, className, onClick, ...props }, ref) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [focused, setFocused] = React.useState(false)
const controlRef = React.useRef<HTMLElement>(null)
return (
<InputBaseContext.Provider
value={{
autoFocus,
controlRef,
disabled,
onFocusedChange: setFocused,
}}>
<Primitive.div
ref={ref}
onClick={composeEventHandlers(onClick, (event) => {
// Based on MUI's <InputBase /> implementation.
// https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460
if (controlRef.current && event.currentTarget === event.target) {
controlRef.current.focus()
}
})}
className={cn(
"flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-0.5 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
disabled && "cursor-not-allowed opacity-50",
className,
)}
{...props}
/>
</InputBaseContext.Provider>
)
},
)
InputBase.displayName = "InputBase"
export const InputBaseFlexWrapper = React.forwardRef<
React.ElementRef<typeof Primitive.div>,
React.ComponentPropsWithoutRef<typeof Primitive.div>
>(({ className, ...props }, ref) => (
<Primitive.div ref={ref} className={cn("flex flex-1 flex-wrap", className)} {...props} />
))
InputBaseFlexWrapper.displayName = "InputBaseFlexWrapper"
export const InputBaseControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ onFocus, onBlur, ...props }, ref) => {
const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBaseContext()
return (
<Slot
ref={composeRefs(controlRef, ref)}
autoFocus={autoFocus}
onFocus={composeEventHandlers(onFocus, () => onFocusedChange(true))}
onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))}
{...{ disabled }}
{...props}
/>
)
})
InputBaseControl.displayName = "InputBaseControl"
export interface InputBaseAdornmentProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean
disablePointerEvents?: boolean
}
export const InputBaseAdornment = React.forwardRef<React.ElementRef<"div">, InputBaseAdornmentProps>(
({ className, disablePointerEvents, asChild, children, ...props }, ref) => {
const Comp = asChild ? Slot : typeof children === "string" ? "p" : "div"
const isAction = React.isValidElement(children) && children.type === InputBaseAdornmentButton
return (
<Comp
ref={ref}
className={cn(
"flex items-center text-muted-foreground [&_svg]:size-4",
(!isAction || disablePointerEvents) && "pointer-events-none",
className,
)}
{...props}>
{children}
</Comp>
)
},
)
InputBaseAdornment.displayName = "InputBaseAdornment"
export const InputBaseAdornmentButton = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentPropsWithoutRef<typeof Button>
>(({ type = "button", variant = "ghost", size = "icon", disabled: disabledProp, className, ...props }, ref) => {
const { disabled } = useInputBaseContext()
return (
<Button
ref={ref}
type={type}
variant={variant}
size={size}
disabled={disabled || disabledProp}
className={cn("size-6", className)}
{...props}
/>
)
})
InputBaseAdornmentButton.displayName = "InputBaseAdornmentButton"
export const InputBaseInput = React.forwardRef<
React.ElementRef<typeof Primitive.input>,
React.ComponentPropsWithoutRef<typeof Primitive.input>
>(({ className, ...props }, ref) => (
<Primitive.input
ref={ref}
className={cn(
"w-full flex-1 bg-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-none disabled:pointer-events-none",
className,
)}
{...props}
/>
))
InputBaseInput.displayName = "InputBaseInput"