import React, { useId, useState } from 'react' import { Drawer, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerBody, } from '../Drawer' import Block from '../Primitives/Block/Block' import Text from '../Text/Text' import { FOUNDATION_THEME } from '../../tokens' 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 type { SingleSelectTokensType } from './singleSelect.tokens' import type { SingleSelectProps } from './types' import { type SelectMenuGroupType, type SelectMenuItemType, SelectMenuSize, SelectMenuVariant, } from '../Select' import SingleSelectTrigger from './SingleSelectTrigger' import { TextInput } from '../Inputs/TextInput' import { TextInputSize } from '../Inputs/TextInput/types' import { Check } from 'lucide-react' import { Skeleton, SkeletonVariant } from '../Skeleton' import { setupAccessibility } from './utils' import { hasExactMatch as checkExactMatch, getFilteredItemsWithCustomValue, hasRenderableSelectItems, } from '../Select/selectUtils' import { useSelectSearchController } from '../Select/useSelectSearchController' import SelectSearchStatus from '../Select/SelectSearchStatus' type MobileSingleSelectProps = SingleSelectProps const map = function getValueLabelMap( groups: SelectMenuGroupType[] ): Record { const map: Record = {} function traverse(items: SelectMenuItemType[]) { for (const item of items) { map[item.value] = item.label if (item.subMenu) { traverse(item.subMenu) } } } for (const group of groups) { traverse(group.items) } return map } const SingleSelectItem = ({ item, isSelected, onSelect, }: { item: SelectMenuItemType isSelected: boolean onSelect: (value: string) => void }) => { return ( { if (!item.disabled) { onSelect(item.value) } }} > {item.slot1 && ( {item.slot1} )} {item.label} {item.slot2 && ( {item.slot2} )} {item.slot3 && ( {item.slot3} )} {item.slot4 && ( {item.slot4} )} {isSelected && ( )} {item.subLabel && ( {item.subLabel} )} ) } const filterMenuItem = ( item: SelectMenuItemType, searchText: string ): SelectMenuItemType | null => { const lower = searchText.toLowerCase() const matches = (item.label && item.label.toLowerCase().includes(lower)) || (item.subLabel && item.subLabel.toLowerCase().includes(lower)) if (item.subMenu) { const filteredSub = item.subMenu .map((sub: SelectMenuItemType) => filterMenuItem(sub, lower)) .filter(Boolean) as SelectMenuItemType[] if (filteredSub.length > 0 || matches) { return { ...item, subMenu: filteredSub } } return null } return matches ? item : null } const filterMenuGroups = ( groups: SelectMenuGroupType[], searchText: string ): SelectMenuGroupType[] => { if (!searchText) return groups return groups .map((group: SelectMenuGroupType) => { const filteredItems = group.items .map((item: SelectMenuItemType) => filterMenuItem(item, searchText) ) .filter(Boolean) as SelectMenuItemType[] if (filteredItems.length === 0) return null return { ...group, items: filteredItems } }) .filter(Boolean) as SelectMenuGroupType[] } const MobileSingleSelect: React.FC = ({ label, subLabel, hintText, required, helpIconText, placeholder, error = false, errorMessage, size = SelectMenuSize.MEDIUM, items = [], name, variant = SelectMenuVariant.CONTAINER, disabled, selected, onSelect, enableSearch, searchPlaceholder = 'Search options...', searchText: controlledSearchText, onSearchChange, isSearchLoading, emptyStateText, slot, customTrigger, inline = false, skeleton = { count: 3, show: false, variant: 'pulse', }, maxTriggerWidth, minTriggerWidth, allowCustomValue = false, customValueLabel = 'Specify', ...rest }) => { const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const singleSelectTokens = useResponsiveTokens('SINGLE_SELECT') const [drawerOpen, setDrawerOpen] = useState(false) const { value: searchText, isControlled: isSearchControlled, isSearchEnabled, shouldFilterInternally, valueForSearchBehavior, dispatchUserValue, resetUncontrolled, } = useSelectSearchController({ controlledValue: controlledSearchText, onValueChange: onSearchChange, explicitShow: enableSearch, existingSurfaceDefault: false, }) const isActiveSearchLoading = isSearchEnabled && Boolean(isSearchLoading) const valueLabelMap = map(items) const hasMatch = React.useMemo( () => checkExactMatch(valueForSearchBehavior, items), [valueForSearchBehavior, items] ) const filteredItems = React.useMemo(() => { const baseFilteredItems = shouldFilterInternally ? filterMenuGroups(items, searchText) : items return getFilteredItemsWithCustomValue( baseFilteredItems, searchText, hasMatch, allowCustomValue, isSearchEnabled && !isSearchLoading, customValueLabel ) }, [ items, searchText, allowCustomValue, hasMatch, isSearchEnabled, isSearchLoading, shouldFilterInternally, customValueLabel, ]) const hasSourceItems = isSearchControlled ? hasRenderableSelectItems(items) : items.length > 0 const hasRenderableItems = isSearchControlled ? hasRenderableSelectItems(filteredItems) : filteredItems.length > 0 const showEmptyState = isSearchControlled ? !hasRenderableItems : !hasSourceItems || (!hasRenderableItems && searchText.length > 0) const isItemSelected = selected.length > 0 const { onFocus, onBlur, ...triggerRest } = rest const isSmallScreenWithLargeSize = isSmallScreen && size === SelectMenuSize.LARGE const generatedId = useId() const { uniqueName, hintTextId, errorMessageId, ariaAttributes } = setupAccessibility({ name, generatedId, label, hintText, error, errorMessage, rest: triggerRest, prefix: 'singleselect-mobile', }) const triggerRestWithoutAria = { ...triggerRest } as Record delete triggerRestWithoutAria['aria-describedby'] delete triggerRestWithoutAria['aria-label'] delete triggerRestWithoutAria['aria-labelledby'] return ( {variant === SelectMenuVariant.CONTAINER && (!isSmallScreen || size !== SelectMenuSize.LARGE) && ( )} { setDrawerOpen(isOpen) if (isOpen) { if (typeof onFocus === 'function') { onFocus( undefined as unknown as React.FocusEvent ) } } else { if (typeof onBlur === 'function') { onBlur( undefined as unknown as React.FocusEvent ) } if (isSearchEnabled) { resetUncontrolled() } } }} > void) | undefined} aria-label={triggerRest['aria-label'] as string | undefined} > {customTrigger || ( setDrawerOpen(true)} slot={slot} variant={variant} isSmallScreenWithLargeSize={ isSmallScreenWithLargeSize } isItemSelected={isItemSelected} inline={inline} error={error} disabled={disabled} {...ariaAttributes} {...triggerRestWithoutAria} /> )} {skeleton.show ? ( {Array.from({ length: skeleton.count || 3, }).map((_, index) => ( ))} ) : ( {isSearchEnabled && ( dispatchUserValue( e.target.value ) } aria-label={ searchPlaceholder || 'Search options' } /> )} {isActiveSearchLoading && !hasRenderableItems ? null : showEmptyState ? ( {emptyStateText || (!hasSourceItems ? 'No items available' : 'No results found')} ) : ( {filteredItems.map( (group, groupId) => ( {group.groupLabel && ( { group.groupLabel } )} {group.items.map( ( item, itemIndex ) => { const isSelected = selected === item.value return ( { onSelect( value ) setDrawerOpen( false ) }} /> ) } )} {groupId !== filteredItems.length - 1 && group.showSeparator && ( )} ) )} )} )} {variant === SelectMenuVariant.CONTAINER && ( )} ) } export default MobileSingleSelect