Select

Examples

Multiple

Code

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 }

Usage

<Select>
  <SelectTrigger className="p-3">
    Select
  </SelectTrigger>
  <SelectContent className="p-3 pt-0">
    <SelectItem value="test-1">Select Item 1</SelectItem>
    <SelectItem value="test-2">Select Item 2</SelectItem>
    <SelectItem value="test-3">Select Item 3</SelectItem>
  </SelectContent>
</Select>