import React, { useCallback, useId, useMemo, useState } from 'react' import { X } from 'lucide-react' import styled from 'styled-components' import type { AriaAttributes } from 'react' import Block from '../Primitives/Block/Block' import PrimitiveButton from '../Primitives/PrimitiveButton/PrimitiveButton' import InputLabels from '../Inputs/utils/InputLabels/InputLabels' import InputFooter from '../Inputs/utils/InputFooter/InputFooter' import { useBreakpoints } from '../../hooks/useBreakPoints' import { BREAKPOINTS } from '../../breakpoints/breakPoints' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useDropdownInteractionLock } from '../../hooks' import { useErrorShake } from '../common/useErrorShake' import { errorShakeAnimation, getErrorShakeStyle, } from '../common/error.animations' import { setupAccessibility } from '../SingleSelect/utils' import { FOUNDATION_THEME } from '../../tokens' import type { MultiSelectV2TokensType } from './multiSelectV2.tokens' import { MultiSelectV2SelectionTagType, MultiSelectV2Size, MultiSelectV2Variant, type MultiSelectV2GroupType, type MultiSelectV2Props, } from './multiSelectV2.types' import MultiSelectV2Menu from './MultiSelectV2Menu' import MultiSelectV2Trigger from './MultiSelectV2Trigger' import MobileMultiSelectV2 from './MobileMultiSelectV2' import { getAllAvailableValues, getMultiSelectBorderRadius, getMultiSelectCrossBorderRadius, getMultiSelectV2ValueLabelMap, } from './utils' import { clampScopeToMaxSelections, emitLegacyScopeChanges, getNextSelectionAfterToggle, getNextSelectionForScope, } from '../shared/multiSelectSelection' const Wrapper = styled(Block)` ${errorShakeAnimation} ` const MultiSelectV2 = ({ selectedValues, onChange, onSelectionChange, items = [], label, subLabel, helpIconText, required, variant = MultiSelectV2Variant.CONTAINER, selectionTagType = MultiSelectV2SelectionTagType.COUNT, slot, hintText, placeholder, size = MultiSelectV2Size.MD, search = { show: true, placeholder: 'Search options...' }, enableSelectAll = false, selectAllText = 'Select All', maxSelections, customTrigger, usePanelOnMobile = true, triggerDimensions = { width: 'auto', minWidth: 'auto', maxWidth: 'auto', }, menuDimensions, menuPosition, inline = false, error = {}, showActionButtons, primaryAction, secondaryAction, showItemDividers = false, showHeaderBorder = false, enableVirtualization = false, virtualListItemHeight = 48, virtualListOverscan = 5, onEndReached, endReachedThreshold, hasMore, loadingComponent, skeleton = { count: 3, show: false, variant: 'pulse', }, allowCustomValue = false, customValueLabel = 'Specify', showClearButton, onClearAllClick, onOpenChange, multiSelectGroupPosition, menuFooter, ...rest }: MultiSelectV2Props) => { const { disabled, name, ...buttonRest } = rest as { disabled?: boolean name?: string onFocus?: React.FocusEventHandler onBlur?: React.FocusEventHandler [key: string]: unknown } const breakpoints = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakpoints.breakPointLabel === 'sm' const isContainer = variant === MultiSelectV2Variant.CONTAINER const multiSelectTokens = useResponsiveTokens('MULTI_SELECT_V2') const [open, setOpen] = useState(false) const safeItems = items ?? [] const valueLabelMap = useMemo( () => getMultiSelectV2ValueLabelMap(safeItems), [safeItems] ) const shouldVirtualize = enableVirtualization && safeItems.length > 20 const shouldShowClearButton = (showClearButton ?? isContainer) && selectedValues.length > 0 const shouldShowActionButtons = showActionButtons !== undefined ? showActionButtons : !!(primaryAction || secondaryAction) const shouldShake = useErrorShake(error?.show || false) const generatedId = useId() const { uniqueName, labelId, hintTextId, errorMessageId, menuId, ariaAttributes, } = setupAccessibility({ name, generatedId, label, hintText, error: error.show, errorMessage: error.message, prefix: 'multiselectv2', needsMenuId: true, }) const triggerAriaAttributes: Pick< AriaAttributes, 'aria-describedby' | 'aria-labelledby' | 'aria-label' | 'aria-invalid' > = { 'aria-describedby': ariaAttributes['aria-describedby'] as | string | undefined, 'aria-labelledby': ariaAttributes['aria-labelledby'] as | string | undefined, 'aria-label': ariaAttributes['aria-label'] as string | undefined, 'aria-invalid': ariaAttributes['aria-invalid'] as boolean | undefined, } const handleOpenChange = useCallback( (isOpen: boolean) => { setOpen(isOpen) onOpenChange?.(isOpen) }, [onOpenChange] ) const handleClearClick = useCallback(() => { if (onClearAllClick) { onClearAllClick() return } onChange?.([]) onSelectionChange?.([]) }, [onClearAllClick, onChange, onSelectionChange]) const handleClearKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() handleClearClick() } }, [handleClearClick] ) useDropdownInteractionLock(!isSmallScreen && open) const handleItemSelect = (value: string) => { const nextSelection = getNextSelectionAfterToggle(selectedValues, value) onChange?.(value) onSelectionChange?.(nextSelection) } const handleBulkSelection = ( selectAll: boolean, filteredItems: MultiSelectV2GroupType[] ) => { const availableValues = getAllAvailableValues(filteredItems) const scopedValues = selectAll ? clampScopeToMaxSelections( selectedValues, availableValues, maxSelections ) : availableValues const nextSelection = getNextSelectionForScope( selectedValues, scopedValues, selectAll ) emitLegacyScopeChanges( selectAll, scopedValues, selectedValues, onChange ? (value) => onChange(value) : undefined ) onSelectionChange?.(nextSelection) } const mobileSharedProps = { selectedValues, onChange, onSelectionChange, items: safeItems, label, subLabel, helpIconText, required, variant, selectionTagType, slot, hintText, placeholder, size, search, enableSelectAll, selectAllText, maxSelections, customTrigger, error, showActionButtons: shouldShowActionButtons, primaryAction: primaryAction ? { ...primaryAction, onClick: () => primaryAction.onClick(selectedValues), } : undefined, secondaryAction, showItemDividers, showHeaderBorder, enableVirtualization: shouldVirtualize, virtualListItemHeight, virtualListOverscan, onEndReached, endReachedThreshold, hasMore, loadingComponent, skeleton, allowCustomValue, customValueLabel, onOpenChange, triggerDimensions, disabled, name, ...buttonRest, } if (isSmallScreen && usePanelOnMobile) { return } const borderRadius = getMultiSelectBorderRadius( size, variant, multiSelectGroupPosition, multiSelectTokens, shouldShowClearButton ) const clearButtonBorder = getMultiSelectCrossBorderRadius( size, variant, multiSelectGroupPosition, multiSelectTokens ) const clearButtonState: 'error' | 'closed' = error.show ? 'error' : 'closed' const clearButtonOutline = multiSelectTokens.trigger.clearButton?.outline?.[clearButtonState] ?? multiSelectTokens.trigger.outline[variant][ error.show ? 'error' : 'closed' ] return ( {isContainer && (!isSmallScreen || size !== MultiSelectV2Size.LG) && ( )} primaryAction.onClick(selectedValues), } : undefined } secondaryAction={secondaryAction} enableVirtualization={shouldVirtualize} virtualListItemHeight={virtualListItemHeight} virtualListOverscan={virtualListOverscan} onEndReached={onEndReached} endReachedThreshold={endReachedThreshold} hasMore={hasMore} loadingComponent={loadingComponent} menuId={menuId} size={size} variant={variant} trigger={ customTrigger || ( ) } allowCustomValue={allowCustomValue} customValueLabel={customValueLabel} menuFooter={menuFooter} /> {shouldShowClearButton && ( )} {isContainer && ( )} ) } MultiSelectV2.displayName = 'MultiSelectV2' export default MultiSelectV2