admin/components/heroui/Autocomplete.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

104 lines
2.9 KiB
TypeScript

'use client'
import type { ReactElement, ReactNode } from 'react'
import { Children, isValidElement, useId, useMemo } from 'react'
import { Input } from '@/components/heroui/Input'
type AutocompleteKey = string | number
interface AutocompleteItemProps {
children?: ReactNode
key?: AutocompleteKey
textValue?: string
}
export function AutocompleteItem(_props: AutocompleteItemProps) {
return null
}
interface AutocompleteProps<T extends object> {
'aria-label'?: string
autoFocus?: boolean
children: ReactNode | ((item: T) => ReactElement<AutocompleteItemProps>)
className?: string
defaultItems?: Iterable<T>
description?: ReactNode
descriptionClassName?: string
endContent?: ReactNode
errorMessage?: ReactNode
isDisabled?: boolean
isInvalid?: boolean
isLoading?: boolean
isReadOnly?: boolean
listboxProps?: object
placeholder?: string
popoverProps?: object
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
selectedKey?: AutocompleteKey | null
size?: 'sm' | 'md' | 'lg'
startContent?: ReactNode
type?: string
validationBehavior?: 'native' | 'aria'
variant?: 'flat' | 'bordered' | 'faded' | 'underlined'
onBlur?: () => void
onInputChange?: (value: string) => void
onSelectionChange?: (key: AutocompleteKey | null) => void
}
function normalizeKey(key: ReactElement['key'], index: number): string {
if (key == null) return String(index)
const value = String(key)
return value.startsWith('.$') ? value.slice(2) : value
}
export function Autocomplete<T extends object>({
children,
defaultItems,
selectedKey,
onInputChange,
onSelectionChange,
...props
}: AutocompleteProps<T>) {
const listId = useId()
const options = useMemo(() => {
const nodes = typeof children === 'function' ? Array.from(defaultItems ?? [], (item) => children(item)) : Children.toArray(children)
return Children.toArray(nodes)
.filter((child): child is ReactElement<AutocompleteItemProps> => isValidElement<AutocompleteItemProps>(child))
.map((child, index) => {
const id = normalizeKey(child.key, index)
const label = child.props.textValue ?? (typeof child.props.children === 'string' ? child.props.children : id)
return { id, label }
})
}, [children, defaultItems])
const selectedLabel = options.find((option) => option.id === String(selectedKey ?? ''))?.label ?? ''
return (
<>
<Input
{...props}
defaultValue={selectedLabel}
list={listId}
onValueChange={(value) => {
onInputChange?.(value)
const match = options.find((option) => option.label === value || option.id === value)
onSelectionChange?.(match?.id ?? null)
}}
/>
<datalist id={listId}>
{options.map((option) => (
<option
key={option.id}
value={option.label}
/>
))}
</datalist>
</>
)
}