/** * Copyright 2025 LY Corporation * * LY Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ 'use client'; import * as React from 'react'; import { Slot, Slottable } from '@radix-ui/react-slot'; import { cva } from 'class-variance-authority'; import { ICON_SIZE } from '../constants'; import { cn } from '../lib/utils'; import type { Radius, Size } from '../types'; import { Icon } from './icon'; import { Popover, PopoverContent, PopoverTrigger } from './popover'; import { ScrollArea, ScrollBar } from './scroll-area'; import { Tag } from './tag'; import useTheme from './use-theme'; type MultiSelectItemType = { value: string; label: React.ReactNode }; type MultiSelectContextType = { disabled: boolean; size: Size; radius: Radius; selectedItems: MultiSelectItemType[]; setSelectedItems: (items: MultiSelectItemType[]) => void; isOpen: boolean; setIsOpen: (open: boolean) => void; toggleOption: (item: MultiSelectItemType) => void; registeredItems: MultiSelectItemType[]; registerItem: (item: MultiSelectItemType) => void; }; const MultiSelectContext = React.createContext< MultiSelectContextType | undefined >(undefined); function useMultiSelectContext() { const ctx = React.useContext(MultiSelectContext); if (!ctx) throw new Error('MultiSelect components must be used within '); return ctx; } interface MultiSelectProps { value?: string[]; defaultValue?: string[]; onValueChange?: (value: string[]) => void; children: React.ReactNode; className?: string; size?: Size; radius?: Radius; disabled?: boolean; } const selectVariants = cva('select', { variants: { size: { small: 'select-small', medium: 'select-medium', large: 'select-large', }, defaultVariants: { size: undefined }, }, }); const MultiSelect = React.forwardRef( function MultiSelect( { value, defaultValue = [], onValueChange, children, className, size, radius, disabled = false, }, ref, ) { const { themeSize, themeRadius } = useTheme(); const [registeredItems, setRegisteredItems] = React.useState< MultiSelectItemType[] >([]); const [selectedValues, setSelectedValues] = React.useState(defaultValue); const [isOpen, setIsOpen] = React.useState(false); const values = value ?? selectedValues; const setValues = (vals: string[]) => { if (onValueChange) onValueChange(vals); if (value === undefined) setSelectedValues(vals); }; const registerItem = React.useCallback((item: MultiSelectItemType) => { setRegisteredItems((prev) => prev.find((i) => i.value === item.value) ? prev : [...prev, item], ); }, []); const selectedItems = React.useMemo( () => values .map((val) => registeredItems.find((item) => item.value === val)) .filter(Boolean) as MultiSelectItemType[], [values, registeredItems], ); const toggleOption = (item: MultiSelectItemType) => { setValues( values.includes(item.value) ? values.filter((v) => v !== item.value) : [...values, item.value], ); }; const hiddenRegisterItems = (
{React.Children.map(children, (child) => { if ( React.isValidElement(child) && (child.type as React.ForwardRefExoticComponent) .displayName === 'MultiSelectItem' ) { return child; } if ( React.isValidElement(child) && (child.props as { children?: React.ReactNode }).children ) { return React.Children.map( (child.props as { children?: React.ReactNode }).children, (c) => c, ); } return null; })}
); return ( setValues(items.map((i) => i.value)), isOpen, setIsOpen, toggleOption, registeredItems, registerItem, }} > {hiddenRegisterItems}
{children}
); }, ); interface MultiSelectTriggerProps extends React.ButtonHTMLAttributes { asChild?: boolean; } const selectTriggerVariants = cva('select-trigger', { variants: { size: { small: 'select-trigger-small', medium: 'select-trigger-medium', large: 'select-trigger-large', }, radius: { small: 'select-trigger-radius-small', medium: 'select-trigger-radius-medium', large: 'select-trigger-radius-large', }, }, defaultVariants: { size: undefined, radius: undefined }, }); const MultiSelectTrigger = React.forwardRef< HTMLButtonElement, MultiSelectTriggerProps >(function MultiSelectTrigger({ asChild, ...props }, ref) { const Comp = asChild ? Slot : MultiSelectTriggerButton; return ( ); }); interface MultiSelectTriggerButtonProps extends React.ButtonHTMLAttributes { className?: string; children?: React.ReactNode; } const MultiSelectTriggerButton = React.forwardRef< HTMLButtonElement, MultiSelectTriggerButtonProps >(function MultiSelectTrigger({ className, children, ...props }, ref) { const { size, radius, disabled } = useMultiSelectContext(); return ( ); }); interface MultiSelectContentProps extends React.HTMLAttributes { maxHeight?: string; } const MultiSelectContent = React.forwardRef< HTMLDivElement, MultiSelectContentProps >(function MultiSelectContent( { className, children, maxHeight = 'auto', ...props }, ref, ) { return (
{children}
); }); const selectItemVariants = cva('select-item', { variants: { check: { left: 'select-item-left', right: 'select-item-right' } }, defaultVariants: { check: 'left' }, }); const selectItemCheckVariants = cva('select-item-check', { variants: { check: { left: 'select-item-check-left', right: 'select-item-check-right' }, }, defaultVariants: { check: 'left' }, }); interface MultiSelectItemProps { value: string; children: React.ReactNode; className?: string; } const MultiSelectItem = React.forwardRef( function MultiSelectItem({ value, children, className }, ref) { const { selectedItems, toggleOption, registerItem } = useMultiSelectContext(); const isSelected = selectedItems.some((item) => item.value === value); // Register this item on mount React.useEffect(() => { registerItem({ value, label: children }); }, [value, children, registerItem]); return (
toggleOption({ value, label: children })} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') toggleOption({ value, label: children }); }} > {isSelected && ( )} {children}
); }, ); MultiSelectItem.displayName = 'MultiSelectItem'; const MultiSelectValue = React.forwardRef< HTMLSpanElement, { placeholder?: React.ReactNode } & React.HTMLAttributes >(function MultiSelectValue({ placeholder, ...props }, ref) { const { selectedItems, size } = useMultiSelectContext(); if (selectedItems.length === 0) { return ( {placeholder} ); } return ( {selectedItems.map((item) => ( {item.label} ))} ); }); export { MultiSelect, MultiSelectTrigger, MultiSelectTriggerButton, MultiSelectContent, MultiSelectItem, MultiSelectValue, };