import { applyPropsToChildrenOfType, findComponentOfType, findComponentsOfType } from '@/utils/manipulateReactComponents'
import { cva } from 'class-variance-authority'
import cn from 'clsx'
import React, { useId } from 'react'
import { HiCheck, HiChevronUpDown } from 'react-icons/hi2'
const ANIMATION_CLASS = 'transition-all duration-200 ease-in-out'
type SelectProps = {
children: React.ReactNode
className?: string
multiple?: boolean
name?: string
}
type SelectItemObject = {
id: string
label: string
value: string
}
type SelectItemsArray = SelectItemObject[]
function extractSelectItemsAsObject(children: React.ReactNode): SelectItemsArray {
const selectContent = findComponentOfType(children, SelectContent)
const selectItemsComponents = findComponentsOfType(selectContent?.props?.children, SelectItem)
const selectItems = selectItemsComponents
.map((item) => {
const id = useId()
const { value, children } = item.props || {}
if (value === undefined) return null
return {
id,
value: String(item?.props?.value),
label: String(children)
}
})
.filter(Boolean) as SelectItemsArray
return selectItems
}
const Select = ({ children, className, multiple, name }: SelectProps) => {
const id = useId()
const selectItems = extractSelectItemsAsObject(children)
children = applyPropsToChildrenOfType(children, { multiple, name: name ?? id, selectItems }, [SelectTrigger, SelectContent, SelectItem], { recursive: true })
return (
<>
<style
dangerouslySetInnerHTML={{
__html: `${
selectItems &&
selectItems
.map(({ value }) => {
return `#${id.replaceAll(':', '\\:')}:has(input:checked[value="${value}"]) .selected-label[data-value="${value}"] { display: inline-flex; }`
})
.join('\n')
}`
}}
/>
<div id={id} className={cn('select', className)}>
{children}
</div>
</>
)
}
Select.displayName = 'Select'
const selectTrigger = cva(
'select-trigger inline-grid z-[1] relative w-full cursor-pointer select-none grid-flow-col grid-cols-[minmax(0,_1fr)_auto] items-center gap-x-2 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1.5 !outline-none'
)
const SelectTrigger: React.FC<{
children: React.ReactNode
className?: string
multiple?: boolean
selectItems?: SelectItemsArray
}> = ({ children, className, selectItems, multiple }) => {
const id = useId()
return (
<>
<input id={id} type="checkbox" className="peer/opened sr-only" />
<label htmlFor={id} className={cn(selectTrigger(), className)}>
<div className="flex flex-wrap gap-1 overflow-hidden">
<span className="inline-flex shrink-0 p-1 [.select:has(input:not(.peer\/opened):checked)_&]:hidden">{children}</span>
{selectItems &&
selectItems.length > 0 &&
selectItems.map(({ value, label }) => (
<span key={value} className={cn('selected-label hidden shrink-0 rounded-md p-1', multiple && 'bg-zinc-700')} data-value={value}>
{label}
</span>
))}
</div>
<HiChevronUpDown className="h-5 w-5 dark:text-zinc-500" />
</label>
<label htmlFor={id} className="fixed inset-0 left-0 top-0 hidden h-full w-full peer-checked/opened:block"></label>
</>
)
}
SelectTrigger.displayName = 'SelectTrigger'
const SelectContent: React.FC<{
children: React.ReactNode
className?: string
context?: any
}> = ({ children, className, context }) => {
return (
<div
className={cn(
'group invisible absolute grid origin-top scale-75 auto-rows-fr rounded-lg p-0.5 opacity-0 peer-checked/opened:visible peer-checked/opened:scale-100 peer-checked/opened:opacity-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white',
ANIMATION_CLASS,
className
)}>
{children}
</div>
)
}
SelectContent.displayName = 'SelectContent'
const SelectItem: React.FC<{
children: React.ReactNode
className?: string
value?: string
selected?: boolean
multiple?: boolean
name?: string
selectItems?: SelectItemsArray
}> = ({ children, className, value, selected = false, multiple, name, selectItems }) => {
const fallbackId = useId()
const thisSelectItem = selectItems?.find((item) => item.value === value)
const id = thisSelectItem?.id || fallbackId
multiple = multiple || false
name = name || id
return (
<div className="flex w-full items-center p-0.5">
<input id={id} type={multiple ? 'checkbox' : 'radio'} className="peer/item sr-only" value={value} name={multiple ? `${name}[]` : name} />
<HiCheck className={cn(ANIMATION_CLASS, 'absolute left-3 z-10 h-5 w-5 opacity-0 peer-checked/item:text-white peer-checked/item:opacity-100')} />
<label
htmlFor={id}
className={cn(
'w-full origin-top cursor-pointer select-none rounded-md bg-zinc-800 py-2 pl-10 pr-5 hover:bg-zinc-700 peer-checked/item:group-[]:bg-zinc-700',
ANIMATION_CLASS,
className
)}>
{children}
</label>
</div>
)
}
SelectItem.displayName = 'SelectItem'
export { Select, SelectContent, SelectItem, SelectTrigger }