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) => (
))}
>
)}
)
}