import * as RadixMenu from '@radix-ui/react-dropdown-menu' import Text from '../Text/Text' import styled from 'styled-components' import type { CSSObject } from 'styled-components' import Block from '../Primitives/Block/Block' import { ChevronRightIcon, Check } from 'lucide-react' import React, { useState } from 'react' import { SelectMenuAlignment, type SelectMenuGroupType, type SelectMenuItemType, type SelectMenuProps, SelectMenuSide, } from './types' import { SearchInputV2 } from '../InputsV2/SearchInputV2' import type { SelectTokensType } from './select.tokens.types' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useTheme } from '../../context/ThemeContext' type TokenizedProps = { $tokens: SelectTokensType $boxShadow?: CSSObject['boxShadow'] } const Content = styled(RadixMenu.Content)( ({ $tokens, $boxShadow }) => ({ backgroundColor: $tokens.container.backgroundColor, color: $tokens.subContent.color, borderRadius: 8, padding: '8px 6px', boxShadow: $boxShadow, zIndex: 49, minWidth: 200, width: 'var(--radix-dropdown-menu-trigger-width)', scrollbarWidth: 'none', scrollbarColor: 'transparent transparent', border: $tokens.container.border, overflowY: 'auto', }) ) const StyledItem = styled(RadixMenu.Item)< { selected: boolean } & TokenizedProps >(({ selected, $tokens }) => ({ alignItems: 'center', gap: 8, padding: '8px 6px', borderRadius: 4, cursor: 'pointer', userSelect: 'none', backgroundColor: selected ? $tokens.item.selectedBackgroundColor : 'transparent', '&:hover': { backgroundColor: $tokens.item.hoverBackgroundColor, }, '&[data-disabled]': { opacity: 0.5, cursor: 'not-allowed', }, '&[data-highlighted]': { border: 'none', outline: 'none', backgroundColor: $tokens.item.highlightedBackgroundColor, }, })) const Sub = styled(RadixMenu.Sub)(() => ({ padding: 0, margin: 0, listStyle: 'none', })) const SubContent = styled(RadixMenu.SubContent)( ({ $tokens, $boxShadow }) => ({ backgroundColor: $tokens.subContent.backgroundColor, color: $tokens.subContent.color, borderRadius: 6, padding: '8px 6px', boxShadow: $boxShadow, zIndex: 49, minWidth: 200, maxWidth: 280, }) ) const SubTrigger = styled(RadixMenu.SubTrigger)( ({ $tokens }) => ({ alignItems: 'center', gap: 8, padding: '6px 8px', borderRadius: 4, cursor: 'pointer', userSelect: 'none', '&:hover': { backgroundColor: $tokens.subTrigger.hoverBackgroundColor, }, '&[data-disabled]': { opacity: 0.5, cursor: 'not-allowed', }, '&[data-highlighted]': { border: 'none', outline: 'none', backgroundColor: $tokens.subTrigger.highlightedBackgroundColor, }, }) ) const SubMenu = ({ item, idx, selectedValue, allowMultiSelect = false, onSelect, tokens, }: { item: SelectMenuItemType idx: number selectedValue: string | string[] | undefined allowMultiSelect?: boolean onSelect?: (value: string | string[]) => void tokens: SelectTokensType }) => { const { foundationTokens } = useTheme() return ( {item.slot1 && ( {item.slot1} )} {item.label} {item.slot2 && ( {item.slot2} )} {item.slot3 && ( {item.slot3} )} {item.slot4 && ( {item.slot4} )} {item.subLabel && ( {item.subLabel} )} {item.subMenu && item.subMenu.map((subItem, subIdx) => ( ))} ) } const Separator = styled(RadixMenu.Separator)( ({ $tokens }) => ({ height: 1, backgroundColor: $tokens.separator.backgroundColor, margin: '6px 0', width: 'calc(100% + 12px)', marginLeft: '-6px', }) ) const Item = ({ item, idx, selectedValue, allowMultiSelect = false, onSelect, tokens, }: { item: SelectMenuItemType idx: number selectedValue: string | string[] | undefined allowMultiSelect?: boolean onSelect?: (value: string | string[]) => void tokens: SelectTokensType }) => { if (item.subMenu) { return ( ) } const isSelected = allowMultiSelect ? Array.isArray(selectedValue) && selectedValue.includes(item.value) : selectedValue === item.value const handleClick = (e: React.MouseEvent) => { if (item.disabled) return // Prevent dropdown from closing in multi-select mode if (allowMultiSelect) { e.preventDefault() e.stopPropagation() } if (allowMultiSelect) { let newSelected: string[] = Array.isArray(selectedValue) ? [...selectedValue] : [] if (newSelected.includes(item.value)) { newSelected = newSelected.filter((v) => v !== item.value) } else { newSelected.push(item.value) } if (onSelect) onSelect(newSelected) } else { if (onSelect) onSelect(item.value) } if (item.onClick) item.onClick() } return ( e.preventDefault() : undefined} > {item.slot1 && ( {item.slot1} )} {item.label} {item.slot2 && ( {item.slot2} )} {item.slot3 && ( {item.slot3} )} {item.slot4 && ( {item.slot4} )} {isSelected && ( )} {item.subLabel && ( {item.subLabel} )} ) } const Label = styled(RadixMenu.Label)(() => ({ padding: '6px 8px', userSelect: 'none', textTransform: 'uppercase', })) // Utility: Recursively filter menu items and groups by search text function filterMenuGroups( groups: SelectMenuGroupType[], searchText: string ): SelectMenuGroupType[] { if (!searchText) return groups const lower = searchText.toLowerCase() return groups .map((group: SelectMenuGroupType) => { // TODO: Should we include the whole group if the label matches? // if (group.label && group.label.toLowerCase().includes(lower)) { // return group; // } const filteredItems = group.items .map((item: SelectMenuItemType) => filterMenuItem(item, lower)) .filter(Boolean) as SelectMenuItemType[] if (filteredItems.length === 0) return null return { ...group, items: filteredItems } }) .filter(Boolean) as SelectMenuGroupType[] } function filterMenuItem( item: SelectMenuItemType, lower: string ): SelectMenuItemType | null { // Check if this item matches const matches = (item.label && item.label.toLowerCase().includes(lower)) || (item.subLabel && item.subLabel.toLowerCase().includes(lower)) // If it has a submenu, filter recursively 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 } // Utility: Recursively override onClick for all menu items and submenus function overrideOnClickInMenuGroups( groups: SelectMenuGroupType[], onSelect?: (value: string) => void ): SelectMenuGroupType[] { return groups.map((group) => ({ ...group, items: group.items.map((item) => overrideOnClickInMenuItem(item, onSelect) ), })) } function overrideOnClickInMenuItem( item: SelectMenuItemType, onSelect?: (value: string) => void ): SelectMenuItemType { const newItem: SelectMenuItemType = { ...item, onClick: onSelect ? () => onSelect(item.value) : undefined, } if (item.subMenu) { newItem.subMenu = item.subMenu.map((sub) => overrideOnClickInMenuItem(sub, onSelect) ) } return newItem } const SelectMenu = ({ trigger, items, asModal = false, alignment = SelectMenuAlignment.CENTER, side = SelectMenuSide.BOTTOM, sideOffset = 8, alignOffset = 0, maxHeight, selected, onSelect, allowMultiSelect = false, enableSearch = false, onOpenChange, }: SelectMenuProps) => { const { foundationTokens } = useTheme() const tokens = useResponsiveTokens('SELECT') const [searchText, setSearchText] = useState('') // Override onClick for all items const itemsWithOnClick = overrideOnClickInMenuGroups(items, onSelect) const filteredItems = filterMenuGroups(itemsWithOnClick, searchText) return ( {trigger} {enableSearch && ( { e.stopPropagation() setSearchText(e.target.value) }} /> )} {filteredItems && filteredItems.map((group, groupId) => ( {group.groupLabel && ( )} {group.items.map((item, itemIndex) => ( ))} {groupId !== filteredItems.length - 1 && group.showSeparator && ( )} ))} ) } export default SelectMenu