import { useCallback, useId, useRef, useState } from 'react' import InputFooter from '../Inputs/utils/InputFooter/InputFooter' import InputLabels from '../Inputs/utils/InputLabels/InputLabels' import Block from '../Primitives/Block/Block' import PrimitiveButton from '../Primitives/PrimitiveButton/PrimitiveButton' import SingleSelectMenu from './SingleSelectMenu' import { ChevronDown } from 'lucide-react' import { SelectMenuAlignment, SelectMenuGroupType, SelectMenuItemType, SelectMenuSize, SelectMenuVariant, type SingleSelectProps, } from './types' import { BREAKPOINTS } from '../../breakpoints/breakPoints' import { useBreakpoints } from '../../hooks/useBreakPoints' import type { SingleSelectTokensType } from './singleSelect.tokens' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import FloatingLabels from '../Inputs/utils/FloatingLabels/FloatingLabels' import { toPixels } from '../../global-utils/GlobalUtils' import MobileSingleSelect from './MobileSingleSelect' import { useErrorShake } from '../common/useErrorShake' import { getErrorShakeStyle, errorShakeAnimation, } from '../common/error.animations' import styled from 'styled-components' import { getBorderRadius, setupAccessibility } from './utils' import { TruncatedTextWithTooltip } from '../common' import { useDropdownInteractionLock } from '../../hooks' const Wrapper = styled(Block)` ${errorShakeAnimation} ` 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 SingleSelect = ({ label, subLabel, hintText, required, helpIconText, placeholder, error = false, errorMessage, size = SelectMenuSize.MEDIUM, items = [], name, variant = SelectMenuVariant.CONTAINER, disabled, selected, onSelect, enableSearch, searchPlaceholder, searchText, onSearchChange, isSearchLoading, emptyStateText, slot, customTrigger, useDrawerOnMobile = true, alignment = SelectMenuAlignment.START, side, sideOffset, alignOffset, minMenuWidth, maxMenuWidth, maxMenuHeight, inline = false, fullWidth = false, enableVirtualization = items.length > 20 ? true : false, virtualListItemHeight, virtualListOverscan, onEndReached, endReachedThreshold, hasMore, loadingComponent, skeleton = { count: 3, show: false, variant: 'pulse', }, maxTriggerWidth, minTriggerWidth, allowCustomValue = false, customValueLabel = 'Specify', singleSelectGroupPosition, allowDeselect = false, menuFooter, ...rest }: SingleSelectProps) => { const { onFocus, onBlur, ...buttonRest } = rest const buttonRestWithoutAria = { ...buttonRest } as Record delete buttonRestWithoutAria['aria-describedby'] delete buttonRestWithoutAria['aria-label'] delete buttonRestWithoutAria['aria-labelledby'] const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const slotRef = useRef(null) const slotWidth = slotRef.current?.offsetWidth const isContainer = variant === SelectMenuVariant.CONTAINER const singleSelectTokens = useResponsiveTokens('SINGLE_SELECT') const [open, setOpen] = useState(false) const { innerWidth } = useBreakpoints() const isMobile = innerWidth < 1024 const valueLabelMap = map(items) const isItemSelected = selected.length > 0 const isSmallScreenWithLargeSize = isSmallScreen && size === SelectMenuSize.LARGE const generatedId = useId() const { uniqueName, hintTextId, errorMessageId, menuId, ariaAttributes } = setupAccessibility({ name, generatedId, label, hintText, error, errorMessage, rest: buttonRest, prefix: 'singleselect', needsMenuId: true, }) const borderRadius = getBorderRadius( size, variant, singleSelectGroupPosition, singleSelectTokens ).borderRadius const paddingX = toPixels( singleSelectTokens.trigger.padding[size][variant].x ) const paddingY = toPixels( singleSelectTokens.trigger.padding[size][variant].y ) const paddingInlineStart = slot && slotWidth ? paddingX + slotWidth + 8 : paddingX const handleOnSelect = useCallback( (val: string) => allowDeselect ? onSelect(val === selected ? '' : val) : onSelect(val), [onSelect, selected, allowDeselect] ) const shouldShake = useErrorShake(error) const callOnFocus = () => { if (typeof onFocus === 'function') { onFocus(undefined as unknown as React.FocusEvent) } } const callOnBlur = () => { if (typeof onBlur === 'function') { onBlur(undefined as unknown as React.FocusEvent) } } useDropdownInteractionLock(!isMobile && open) if (isMobile && useDrawerOnMobile) { return ( ) } return ( {isContainer && (!isSmallScreen || size !== SelectMenuSize.LARGE) && ( )} { setOpen(isOpen) if (isOpen) { callOnFocus() } else { callOnBlur() } }} items={items} selected={selected} onSelect={(value) => { handleOnSelect(value) requestAnimationFrame(() => { setOpen(false) }) }} disabled={disabled} minMenuWidth={minMenuWidth} maxMenuWidth={maxMenuWidth} maxMenuHeight={maxMenuHeight} alignment={alignment} side={side} sideOffset={sideOffset} alignOffset={alignOffset} enableSearch={enableSearch} searchPlaceholder={searchPlaceholder} searchText={searchText} onSearchChange={onSearchChange} isSearchLoading={isSearchLoading} emptyStateText={emptyStateText} enableVirtualization={enableVirtualization} virtualListItemHeight={virtualListItemHeight} virtualListOverscan={virtualListOverscan} onEndReached={onEndReached} endReachedThreshold={endReachedThreshold} hasMore={hasMore} loadingComponent={loadingComponent} allowCustomValue={allowCustomValue} customValueLabel={customValueLabel} menuId={menuId} menuFooter={menuFooter} trigger={ customTrigger || ( { callOnFocus() }} onBlur={() => { callOnBlur() }} {...buttonRestWithoutAria} > {slot && ( {slot} )} {isSmallScreenWithLargeSize && isContainer ? ( {selected && ( )} ) : ( )} ) } size={size} variant={variant} /> {isContainer && ( )} ) } export default SingleSelect