import { useId, useMemo, useState } from 'react' import { Drawer, DrawerBody, DrawerContent, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, } from '../../Drawer' import Block from '../../Primitives/Block/Block' import Text from '../../Text/Text' import Button from '../../Button/Button' import { ButtonSize, ButtonType } from '../../Button/types' import { Checkbox } from '../../Checkbox' import { CheckboxSize } from '../../Checkbox/types' import { TextInput } from '../../Inputs/TextInput' import { TextInputSize } from '../../Inputs/TextInput/types' import InputFooter from '../../Inputs/utils/InputFooter/InputFooter' import InputLabels from '../../Inputs/utils/InputLabels/InputLabels' import { Skeleton, SkeletonVariant } from '../../Skeleton' import VirtualList from '../../VirtualList/VirtualList' import SelectItemV2 from '../../SelectV2/SelectItemV2' import { hasExactMatch, getFilteredItemsWithCustomValue, hasRenderableSelectItems, } from '../../Select/selectUtils' import { setupAccessibility } from '../../SingleSelect/utils' import { useBreakpoints } from '../../../hooks/useBreakPoints' import { BREAKPOINTS } from '../../../breakpoints/breakPoints' import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens' import type { MultiSelectV2TokensType } from '../multiSelectV2.tokens' import { MultiSelectV2SelectionTagType, MultiSelectV2Size, MultiSelectV2Variant, type MultiSelectV2Props, } from '../multiSelectV2.types' import MultiSelectV2Trigger from '../MultiSelectV2Trigger' import { filterMultiSelectV2MenuGroups, getAllAvailableValues, getSelectAllState, getMultiSelectV2ValueLabelMap, } from '../utils' import { flattenMobileMultiSelectV2Groups, isSelectableItem, toVirtualListItems, type FlattenedMobileMultiSelectV2Item, } from './mobileMultiSelectV2.utils' import { clampScopeToMaxSelections, emitLegacyScopeChanges, getNextSelectionAfterToggle, getNextSelectionForScope, } from '../../shared/multiSelectSelection' import { useSelectSearchController } from '../../Select/useSelectSearchController' import SelectSearchStatus, { SELECT_SEARCH_STATUS_HEIGHT, } from '../../Select/SelectSearchStatus' const MobileMultiSelectV2 = ({ selectedValues, onChange, onSelectionChange, items, label, subLabel, helpIconText, required = false, variant = MultiSelectV2Variant.CONTAINER, selectionTagType = MultiSelectV2SelectionTagType.COUNT, slot, hintText, placeholder, size = MultiSelectV2Size.MD, search, enableSelectAll = false, selectAllText = 'Select All', maxSelections, customTrigger, error = {}, showActionButtons = true, primaryAction, secondaryAction, showItemDividers = false, showHeaderBorder = false, enableVirtualization = false, virtualListItemHeight = 56, virtualListOverscan = 5, onEndReached, endReachedThreshold, hasMore, skeleton = { count: 3, show: false, variant: 'pulse', }, allowCustomValue = false, customValueLabel = 'Specify', onOpenChange, triggerDimensions, inline = false, disabled = false, name, ...buttonRest }: MultiSelectV2Props) => { const safeItems = items ?? [] const [open, setOpen] = useState(false) const multiSelectTokens = useResponsiveTokens('MULTI_SELECT_V2') const valueLabelMap = useMemo( () => getMultiSelectV2ValueLabelMap(safeItems), [safeItems] ) const generatedId = useId() const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const isContainer = variant === MultiSelectV2Variant.CONTAINER const searchPlaceholder = search?.placeholder ?? 'Search options...' const { value: searchText, isControlled: isSearchControlled, isSearchEnabled, shouldFilterInternally, valueForSearchBehavior, dispatchUserValue, resetUncontrolled, } = useSelectSearchController({ controlledValue: search?.searchText, onValueChange: search?.onSearchChange, explicitShow: search?.show, existingSurfaceDefault: false, }) const isActiveSearchLoading = isSearchEnabled && Boolean(search?.isSearchLoading) const { uniqueName, labelId, hintTextId, errorMessageId, ariaAttributes } = setupAccessibility({ name, generatedId, label, hintText, error: error.show, errorMessage: error.message, rest: buttonRest, prefix: 'multiselectv2-mobile', }) const hasMatch = useMemo( () => hasExactMatch(valueForSearchBehavior, safeItems), [valueForSearchBehavior, safeItems] ) const filteredItems = useMemo(() => { const baseFilteredItems = shouldFilterInternally ? filterMultiSelectV2MenuGroups(safeItems, searchText) : safeItems return getFilteredItemsWithCustomValue( baseFilteredItems, searchText, hasMatch, allowCustomValue, isSearchEnabled && !search?.isSearchLoading, customValueLabel ) }, [ safeItems, searchText, hasMatch, allowCustomValue, isSearchEnabled, search?.isSearchLoading, shouldFilterInternally, customValueLabel, ]) const hasSourceItems = isSearchControlled ? hasRenderableSelectItems(safeItems) : safeItems.length > 0 const hasRenderableItems = isSearchControlled ? hasRenderableSelectItems(filteredItems) : filteredItems.length > 0 const showEmptyState = isSearchControlled ? !hasRenderableItems : !hasSourceItems || (!hasRenderableItems && searchText.length > 0) const searchStatusHeight = isActiveSearchLoading && hasRenderableItems ? SELECT_SEARCH_STATUS_HEIGHT : 0 const flattenedItems = useMemo( () => flattenMobileMultiSelectV2Groups(filteredItems, enableSelectAll), [filteredItems, enableSelectAll] ) const selectAllItems = isSearchControlled ? safeItems : filteredItems const selectableValues = useMemo( () => getAllAvailableValues(selectAllItems), [selectAllItems] ) const { allSelected, someSelected } = getSelectAllState( selectedValues, selectableValues ) const handleOpenChange = (nextOpen: boolean) => { setOpen(nextOpen) if (!nextOpen && isSearchEnabled) { resetUncontrolled() } onOpenChange?.(nextOpen) } const handleSelect = (value: string) => { const selected = selectedValues.includes(value) const maxReached = maxSelections !== undefined && selectedValues.length >= maxSelections && !selected if (maxReached || !isSelectableItem(value, filteredItems)) { return } const nextSelection = getNextSelectionAfterToggle(selectedValues, value) onChange?.(value) onSelectionChange?.(nextSelection) } const getSelectAllScope = (selectAll: boolean) => selectAll ? clampScopeToMaxSelections( selectedValues, selectableValues, maxSelections ) : selectableValues const handleLegacySelectAll = ( selectAll: boolean, scopedValues: string[] ) => { emitLegacyScopeChanges( selectAll, scopedValues, selectedValues, onChange ? (newValue) => onChange(newValue) : undefined ) } const handleSelectAllGesture = () => { const selectAll = !allSelected const scopedValues = getSelectAllScope(selectAll) const nextSelection = getNextSelectionForScope( selectedValues, scopedValues, selectAll ) handleLegacySelectAll(selectAll, scopedValues) onSelectionChange?.(nextSelection) } const triggerAriaAttributes = { '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 renderMenuItem = (flatItem: FlattenedMobileMultiSelectV2Item) => { if (flatItem.type === 'selectAll') { return ( {selectAllText} { const selectAll = checked === true || checked === 'indeterminate' handleLegacySelectAll( selectAll, getSelectAllScope(selectAll) ) }} /> ) } if (flatItem.type === 'label') { return ( {flatItem.label} ) } if (flatItem.type === 'separator') { return ( ) } if (flatItem.type === 'item' && flatItem.item) { return ( ) } return null } return ( {isContainer && (!isSmallScreen || size !== MultiSelectV2Size.LG) && ( )} {customTrigger || ( )} {label || 'Select options'} {skeleton.show ? ( {Array.from({ length: skeleton.count || 3, }).map((_, index) => ( ))} ) : ( {isSearchEnabled && ( dispatchUserValue( e.target.value ) } aria-label={searchPlaceholder} /> )} {isActiveSearchLoading && !hasRenderableItems ? null : showEmptyState ? ( {search?.emptyStateText || (!hasSourceItems ? 'No items available' : 'No results found')} ) : enableVirtualization ? ( renderMenuItem( item as FlattenedMobileMultiSelectV2Item ) } /> ) : ( {flattenedItems.map( (flatItem) => ( {renderMenuItem( flatItem )} ) )} )} {showActionButtons && (primaryAction || secondaryAction) && ( {secondaryAction && (