import React from "react"; import { UseComboboxGetMenuPropsOptions, UseComboboxGetItemPropsOptions, } from "downshift"; import { cn } from "#/utils/utils"; import { dropdownMenuListClassName } from "#/utils/dropdown-classes"; export interface GenericDropdownMenuProps { isOpen: boolean; filteredItems: T[]; inputValue: string; highlightedIndex: number; selectedItem: T | null; getMenuProps: ( options?: UseComboboxGetMenuPropsOptions & Options, ) => any; // eslint-disable-line @typescript-eslint/no-explicit-any getItemProps: ( options: UseComboboxGetItemPropsOptions & Options, ) => any; // eslint-disable-line @typescript-eslint/no-explicit-any onScroll?: (event: React.UIEvent) => void; menuRef?: React.RefObject; renderItem: ( item: T, index: number, highlightedIndex: number, selectedItem: T | null, getItemProps: ( options: UseComboboxGetItemPropsOptions & Options, ) => any, // eslint-disable-line @typescript-eslint/no-explicit-any ) => React.ReactNode; /** * Optional presentational node rendered immediately BEFORE an item (e.g. a * group header). It is a sibling of the item, not part of downshift's `items` * array, so it consumes no item index — mirroring the `numberOfRecentItems` * divider. The consumer owns when a prefix appears (e.g. at group boundaries). */ renderItemPrefix?: (item: T, index: number) => React.ReactNode; renderEmptyState: (inputValue: string) => React.ReactNode; stickyTopItem?: React.ReactNode; stickyFooterItem?: React.ReactNode; testId?: string; numberOfRecentItems?: number; itemKey: (item: T) => string | number; } export function GenericDropdownMenu({ isOpen, filteredItems, inputValue, highlightedIndex, selectedItem, getMenuProps, getItemProps, onScroll, menuRef, renderItem, renderItemPrefix, renderEmptyState, stickyTopItem, stickyFooterItem, testId, numberOfRecentItems = 0, itemKey, }: GenericDropdownMenuProps) { const hasItems = filteredItems.length > 0; const showEmptyState = !hasItems && !stickyTopItem && !stickyFooterItem; // Always render the menu container (even when closed) so getMenuProps is always called // This prevents the downshift warning about forgetting to call getMenuProps if (!isOpen) { return (
); } return (
    {showEmptyState ? ( renderEmptyState(inputValue) ) : ( <> {stickyTopItem} {filteredItems.map((item, index) => { const key = itemKey(item); return ( {renderItemPrefix?.(item, index)} {renderItem( item, index, highlightedIndex, selectedItem, getItemProps, )} {numberOfRecentItems > 0 && index === numberOfRecentItems - 1 && (
{stickyFooterItem && (
{stickyFooterItem}
)}
); }