"use client" import React, { Children, ComponentProps, ElementRef, forwardRef, useLayoutEffect, useMemo, useState, } from "react" import { createContext, useContextSelector } from "use-context-selector" import { ChevronLeft } from "../../icons" import { InputVariantProps, inputVariants } from "../../styles" import { classNames } from "../../utils" import { Dropdown, DropdownItem, DropdownItemAvatar, DropdownItemContent, DropdownItemDescription, DropdownItemTitle, DropdownProps, DropdownSize, } from "../Dropdown" import { UnstyledButton, UnstyledButtonProps } from "../UnstyledButton" type SelectPropsBase = Omit & InputVariantProps & { name?: string placeholder?: React.ReactNode onValueChange?: (value: string) => void overrides?: { Content?: { className?: string } } } type SelectPropsSingleSelection = SelectPropsBase & { selection?: "single" /** * The value of the Select. Value must be a subset of the value of the SelectItems rendered as children. * In multiple selection mode, this should be undefined. */ value?: string /** * A function that renders the current value of the Select. Defaults to value's SelectItem.Title or the value itself. * In multiple selection mode, this should be undefined. */ renderValue?: (value: string) => React.ReactNode } type SelectPropsMultipleSelection = SelectPropsBase & { /** * When selection mode is multiple, the Select does not close when an item is selected. */ selection: "multiple" /** * Placeholder is required when selection mode is multiple. */ placeholder: string value?: undefined renderValue?: undefined } type SelectSize = "xs" | "sm" | "md" | "lg" export type SelectProps = ( | SelectPropsSingleSelection | SelectPropsMultipleSelection ) & { size?: SelectSize; dropdownSize?: DropdownSize } type SelectContextType = Pick & { itemRefs: HTMLButtonElement[] setItemRefs: React.Dispatch> forceMount: true | undefined setForceMount: (forceMount: true | undefined) => void } const SelectContext = createContext({ selection: "single", itemRefs: [], setItemRefs: () => {}, onValueChange: () => {}, forceMount: undefined, setForceMount: () => {}, }) const SelectBase = forwardRef< ElementRef, Omit >(function Select( { value, children, className, placeholder, disabled, error, id, name, renderValue, variant = "default", size = "sm", dropdownSize = "sm", ...props }, ref, ) { const [itemRefs, setItemRefs, forceMount, setForceMount] = useContextSelector( SelectContext, v => [v.itemRefs, v.setItemRefs, v.forceMount, v.setForceMount], ) // This is a hack to force the dropdown to initially mount on render so we can properly // set the itemRefs. This is so we can render the correct value in the trigger. // Furthermore, we re-mount when children change/values are added. useLayoutEffect(() => { setForceMount(undefined) }, [forceMount, setForceMount]) useLayoutEffect(() => { // Clean up item refs every time children change so we don't accumulate refs indefinitely setItemRefs([]) if (forceMount) { setForceMount(undefined) } else { setForceMount(true) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [Children.toArray(children).length]) const displayText = useMemo(() => { return itemRefs .find(el => el.value === value) ?.querySelector("[data-item-text]")?.textContent }, [itemRefs, value]) return ( {renderValue && value ? renderValue(value) : displayText || value || placeholder} ) }) const SelectBaseWithContext = forwardRef< ElementRef, SelectProps >(function SelectWithContext( { selection = "single", onValueChange, ...rest }, ref, ) { const [itemRefs, setItemRefs] = useState([]) const [forceMount, setForceMount] = useState(true) return ( ) }) type SelectItemProps = ComponentProps & { value: string } export type SelectButtonProps = InputVariantProps & UnstyledButtonProps export const SelectButton = forwardRef< ElementRef, SelectButtonProps >(function SelectButton( { children, className, disabled, error, variant, size, ...rest }, ref, ) { return ( svg]:-rotate-90 [&[data-state=open]>svg]:rotate-90", className, )} disabled={disabled} ref={ref} {...rest} > {children} ) }) const SelectItemBase = forwardRef< ElementRef, SelectItemProps >(function SelectItem({ value, onSelect, ...rest }, ref) { const [itemRefs, setItemRefs, onValueChange, selection] = useContextSelector( SelectContext, v => [v.itemRefs, v.setItemRefs, v.onValueChange, v.selection], ) return ( { if (ref) { if (typeof ref === "function") { ref(el) } else { ref.current = el } } if ( el instanceof HTMLButtonElement && !itemRefs.map(item => item.value).includes(el.value) ) { el.value = value setItemRefs(itemRefs => [...itemRefs, el]) } }} onSelect={e => { onSelect?.(e) onValueChange?.(value) // Prevent the dropdown from being closed when selecting an item for the multiple case if (selection === "multiple") { e.preventDefault() } }} /> ) }) export const SelectItemAvatar = DropdownItemAvatar export const SelectItemTitle = ( props: ComponentProps, ) => { return } export const SelectItemDescription = DropdownItemDescription export const SelectItemContent = DropdownItemContent export const SelectItem = Object.assign(SelectItemBase, { Avatar: SelectItemAvatar, Title: SelectItemTitle, Description: SelectItemDescription, Content: SelectItemContent, }) /** * Displays a list of options for the user to pick from — triggered by a button. */ export const Select = Object.assign(SelectBaseWithContext, { Item: SelectItem, })