import styled from 'styled-components' import * as RadixMenu from '@radix-ui/react-dropdown-menu' import { MenuItemActionType, type MenuItemType, MenuItemVariant } from './types' import Block from '../Primitives/Block/Block' import Text from '../Text/Text' // eslint-disable-next-line import-x/no-cycle -- intentional recursion: SubMenu renders MenuItem which renders SubMenu import MenuItem from './MenuItem' import { ChevronRightIcon, Search } from 'lucide-react' import { getMenuItemStateToken, type MenuItemStates, type MenuTokensType, } from './menu.tokens' import PrimitiveText from '../Primitives/PrimitiveText/PrimitiveText' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import SearchInput from '../Inputs/SearchInput/SearchInput' import { FOUNDATION_THEME } from '../../tokens' import { filterMenuItem, defaultSearchSortFn } from './utils' import React, { useState, useRef, useMemo, useCallback } from 'react' import { submenuContentAnimations } from './menu.animations' import { VirtualList, type VirtualListItem } from '../VirtualList' import type { MenuSelectionMode, MenuSelectionStyle } from './selection' const MenuSlot = ({ slot }: { slot: React.ReactNode }) => { return ( {slot} ) } const SubContent = styled(RadixMenu.SubContent)` box-shadow: ${FOUNDATION_THEME.shadows.sm}; z-index: 101; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; scrollbar-color: transparent transparent; padding-bottom: 6px; border-radius: 8px; ${submenuContentAnimations} ` const getBgColor = ( state: MenuItemStates, menuTokens: MenuTokensType, item: MenuItemType ) => { const bg = menuTokens.item.backgroundColor // check for variant if ( item.variant === MenuItemVariant.DEFAULT || (item.subMenu && item.subMenu.length > 0) ) { if (!item.disabled) { return getMenuItemStateToken(bg.default.enabled, state) } else { return getMenuItemStateToken(bg.default.disabled, state) } } else { // check for action type if (item.actionType === undefined) { item.actionType = MenuItemActionType.PRIMARY } if (item.actionType === MenuItemActionType.PRIMARY) { if (!item.disabled) { return getMenuItemStateToken(bg.action.primary.enabled, state) } else { return getMenuItemStateToken(bg.action.primary.disabled, state) } } else { if (!item.disabled) { return getMenuItemStateToken(bg.action.danger.enabled, state) } else { return getMenuItemStateToken(bg.action.danger.disabled, state) } } } } const getLabelColor = ( state: MenuItemStates, menuTokens: MenuTokensType, item: MenuItemType ) => { const bg = menuTokens.item.option.color // check for variant if ( item.variant === MenuItemVariant.DEFAULT || (item.subMenu && item.subMenu.length > 0) ) { if (!item.disabled) { return getMenuItemStateToken(bg.default.enabled, state) } else { return getMenuItemStateToken(bg.default.disabled, state) } } else { // check for action type if (item.actionType === undefined) { item.actionType = MenuItemActionType.PRIMARY } if (item.actionType === MenuItemActionType.PRIMARY) { if (!item.disabled) { return getMenuItemStateToken(bg.action.primary.enabled, state) } else { return getMenuItemStateToken(bg.action.primary.disabled, state) } } else { if (!item.disabled) { return getMenuItemStateToken(bg.action.danger.enabled, state) } else { return getMenuItemStateToken(bg.action.danger.disabled, state) } } } } const getSubLabelColor = ( state: MenuItemStates, menuTokens: MenuTokensType, item: MenuItemType ) => { const bg = menuTokens.item.description.color // check for variant if ( item.variant === MenuItemVariant.DEFAULT || (item.subMenu && item.subMenu.length > 0) ) { if (!item.disabled) { return getMenuItemStateToken(bg.default.enabled, state) } else { return getMenuItemStateToken(bg.default.disabled, state) } } else { // check for action type if (item.actionType === undefined) { item.actionType = MenuItemActionType.PRIMARY } if (item.actionType === MenuItemActionType.PRIMARY) { if (!item.disabled) { return getMenuItemStateToken(bg.action.primary.enabled, state) } else { return getMenuItemStateToken(bg.action.primary.disabled, state) } } else { if (!item.disabled) { return getMenuItemStateToken(bg.action.danger.enabled, state) } else { return getMenuItemStateToken(bg.action.danger.disabled, state) } } } } export const SubMenu = ({ item, idx, maxHeight, selectionStyle, selectionMode, }: { item: MenuItemType idx: number maxHeight?: number selectionStyle?: MenuSelectionStyle selectionMode?: MenuSelectionMode }) => { const menuTokens = useResponsiveTokens('MENU') const [searchText, setSearchText] = useState('') const searchInputRef = useRef(null) const { enableSubMenuVirtualScrolling = false, subMenuVirtualItemHeight = 40, subMenuVirtualOverscan = 5, subMenuVirtualScrollThreshold = 20, } = item const filteredSubMenuItems = React.useMemo(() => { if (!item.subMenu) return [] if (!searchText || !item.enableSubMenuSearch) return item.subMenu const lower = searchText.toLowerCase() const matched = item.subMenu .map((subItem) => filterMenuItem(subItem, lower)) .filter(Boolean) as MenuItemType[] const sortFn = item.subMenuSearchSortFn ?? defaultSearchSortFn return sortFn(matched, searchText) }, [ item.subMenu, searchText, item.enableSubMenuSearch, item.subMenuSearchSortFn, ]) const totalItemCount = useMemo(() => { return filteredSubMenuItems.length }, [filteredSubMenuItems]) const shouldUseVirtualScrolling = enableSubMenuVirtualScrolling && totalItemCount >= subMenuVirtualScrollThreshold const virtualListItems = useMemo(() => { const virtualItems: VirtualListItem[] = [] filteredSubMenuItems.forEach((subItem, subIdx) => { virtualItems.push({ id: `submenu-item-${idx}-${subIdx}`, data: { item: subItem, idx: subIdx, }, }) }) return virtualItems }, [filteredSubMenuItems, idx]) const renderVirtualItem = useCallback( ({ item }: { item: VirtualListItem; index: number }) => { const data = item.data as { item: MenuItemType; idx: number } return ( ) }, [selectionMode, selectionStyle] ) const handleSearchChange = (e: React.ChangeEvent) => { setSearchText(e.target.value) } return ( {item.slot1 && } {item.label} {item.slot2 && } {item.slot3 && } {item.slot4 && } {item.subLabel && ( {item.subLabel} )} { if ( item.enableSubMenuSearch && searchText && searchInputRef.current ) { if (e.target !== searchInputRef.current) { e.preventDefault() searchInputRef.current.focus() } } }} > {item.enableSubMenuSearch && ( } placeholder={ item.subMenuSearchPlaceholder || 'Search...' } value={searchText} onChange={handleSearchChange} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation() e.preventDefault() item.onSubMenuSearchEnter?.( searchText, filteredSubMenuItems ) return } if (e.key.length === 1) { e.stopPropagation() } }} /> )} {shouldUseVirtualScrolling ? ( ) : ( <> {filteredSubMenuItems.map((subItem, subIdx) => ( ))} )} ) }