// @flow strict import * as React from 'react'; // $FlowIssue[nonstrict-import] react-window import {FixedSizeList} from 'react-window'; import type {MenuClassNames, MenuLabelTooltip} from '../../types/menu'; import {classify} from '../../utils/classify'; import { getFilteredComposeOptionsFromSearchText, getFilteredComposeOptionsResultText, getFilteredGroupTitleOptionsFromSearchText, getFilteredGroupTitleOptionsResultText, getFilteredOptionsFromSearchText, getFilteredOptionsResultText, } from '../../utils/menu'; import type {IconType} from '../Icon/Icon'; import {SearchInput} from '../SearchInput'; import {FormLabelSmall} from '../Text'; import {MenuOptionButton} from './MenuOptionButton'; import css from './Menu.module.css'; type ClassNames = MenuClassNames; type OptionClassNames = $ReadOnly<{ wrapper?: string, }>; export type Virtualization = $ReadOnly<{ enable: boolean, itemHeight?: number, menuHeight?: number, }>; export type MenuOption = { key: string, classNames?: OptionClassNames, label?: string, secondaryLabel?: string, customComponent?: React.Node, iconLeft?: string, iconLeftType?: IconType, iconRight?: string, iconRightType?: IconType, disabled?: boolean, optionSize?: MenuSizeTypes, optionVariant?: MenuOptionsVariant, keepMenuOpenOnOptionSelect?: boolean, indeterminate?: boolean, }; // Render first available option set export type BaseMenuProps = { onSelect?: (option: MenuOption, ?SyntheticEvent) => mixed, selectedOption?: ?MenuOption, optionsVariant?: MenuOptionsVariant, selectedKeys?: Array, classNames?: ClassNames, size?: MenuSizeTypes, width?: string, menuDisabled?: boolean, isFluid?: boolean, // onTabOut is a callback function that is called when // the user navigates outside of the menu using the tab key. onTabOut?: () => mixed, allowSearch?: boolean, // A function that resolves the label for a MenuOption. // It takes a MenuOption as a parameter and returns either a string or a React Node. resolveLabel?: (option: MenuOption) => string | React.Node, // A function that resolves the secondaryLabel for a MenuOption. // It takes a MenuOption as a parameter and returns either a string or a React Node. resolveSecondaryLabel?: (option: MenuOption) => string | React.Node, // When virtualization is enabled, the MenuOptionButtons will be rendered only when they are present in the Menu's viewport virtualization?: Virtualization, header?: React.Node, footer?: React.Node, showResultText?: boolean, staticLabels?: { RESULT?: string, RESULTS?: string, SEARCH_PLACEHOLDER?: string, }, showLabelTooltip?: MenuLabelTooltip, allowWrap?: boolean, }; export type MenuOptionTypes = { options?: Array, composeOptions?: Array>, groupTitleOptions?: Array, }; export type MenuSizeTypes = 'medium' | 'small'; export type MenuOptionsVariant = 'checkbox' | 'radio' | 'normal'; export type MenuGroupTitleOption = { groupTitle?: React.Node, options?: Array, showLineDivider?: boolean, }; export type MenuProps = { ...BaseMenuProps, ...MenuOptionTypes, }; export type RenderOptionProps = { ...MenuProps, searchText?: string, }; const menuSizeMedium = 276, menuSizeSmall = 228; const buttonSizeMedium = 40, buttonSizeSmall = 32; const RenderOption = ({ options, composeOptions, groupTitleOptions, classNames, searchText = '', showResultText = true, staticLabels = { RESULT: 'result', RESULTS: 'results', SEARCH_PLACEHOLDER: 'Search...', }, ...restProps }: RenderOptionProps): React.Node => { const { allowSearch, size, virtualization = { enable: false, menuHeight: null, itemHeight: null, }, } = restProps; if (options && Array.isArray(options) && options.length) { const optionsFiltered = !allowSearch ? options : getFilteredOptionsFromSearchText(options, searchText); const finalResultText = !allowSearch ? '' : getFilteredOptionsResultText(optionsFiltered, staticLabels); const { enable: isVirtualizationEnabled, menuHeight, itemHeight, } = virtualization; return ( <> {allowSearch && showResultText && ( {finalResultText} )} {virtualization && isVirtualizationEnabled ? ( {({index: idx, style}) => { const buttonOption = optionsFiltered[idx]; return ( ); }} ) : ( optionsFiltered.map((option, idx) => ( )) )} ); } if ( composeOptions && Array.isArray(composeOptions) && composeOptions.length ) { const optionsFiltered = !allowSearch ? composeOptions : getFilteredComposeOptionsFromSearchText(composeOptions, searchText); const finalResultText = !allowSearch ? '' : getFilteredComposeOptionsResultText(optionsFiltered, staticLabels); return ( <> {allowSearch && showResultText && ( {finalResultText} )} {optionsFiltered.map((composeMenuOptions, index) => ( // eslint-disable-next-line react/no-array-index-key {composeMenuOptions.map((option, idx) => ( ))} ))} ); } if ( groupTitleOptions && Array.isArray(groupTitleOptions) && groupTitleOptions.length ) { const optionsFiltered = !allowSearch ? groupTitleOptions : getFilteredGroupTitleOptionsFromSearchText( groupTitleOptions, searchText, ); const finalResultText = !allowSearch ? '' : getFilteredGroupTitleOptionsResultText(optionsFiltered, staticLabels); return ( <> {allowSearch && showResultText && ( {finalResultText} )} {optionsFiltered.map((optionsGroup, index) => ( // eslint-disable-next-line react/no-array-index-key {!!optionsGroup.groupTitle && (
{optionsGroup.groupTitle}
)} {optionsGroup.options?.map((option, idx) => ( ))}
))} ); } return <>; }; export const Menu: React$AbstractComponent = React.forwardRef( (props: MenuProps, ref): React.Node => { const { classNames, size = 'medium', width, isFluid = true, allowSearch, virtualization = { enable: false, menuHeight: null, itemHeight: null, }, header, footer, staticLabels, } = props; const [searchText, setSearchText] = React.useState(''); const {menuHeight} = virtualization; const hasHeader = header ? true : false; const hasFooter = footer ? true : false; return (
{hasHeader && (
{header}
)} {allowSearch && ( setSearchText(e.target.value)} onClear={() => setSearchText('')} size={size} placeholder={staticLabels?.SEARCH_PLACEHOLDER ?? 'Search...'} /> )} {hasFooter && (
{footer}
)}
); }, );