import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from 'react'; import type { ElementType, HTMLAttributes, KeyboardEvent, ReactNode, SyntheticEvent } from 'react'; import { useToggle } from 'react-use'; import clsx from 'clsx'; import ClickAwayListener from '@mui/base/ClickAwayListener'; import MuiSelect from '@mui/material/Select'; import type { SelectProps as MuiSelectProps } from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import Box from '@mui/material/Box'; import InputLabel from '@mui/material/InputLabel'; import type { InputLabelProps } from '@mui/material/InputLabel'; import type { PaperProps } from '@mui/material/Paper'; import { ASSETS_URL } from '../../../consts/common'; import { CustomIcon } from '../../custom-icon'; import { MenuContent } from '../../dropdown-menu'; import { TextWithTooltip } from '../../text-with-tooltip'; import type { DropdownMenuItem, DropDownMenuProps, MenuContentProps } from '../../dropdown-menu'; import createClasses from './styles'; import { unstable_useControlled as useControlled } from '@mui/utils'; const textSeparator = ', '; export interface SelectProps extends Omit { /** * Props which will be passed into the menu content component. */ menuContentProps: MenuContentProps; /** * The default element value. Use when the component is not controlled. */ defaultValue?: string; classes?: Partial>; chips?: boolean; /** * Whether to show a tooltip with a list of limited items. */ showLimitItemsTooltip?: boolean; /** * Whether to show a select field clear button. */ showSelectClearButton?: boolean; /** * A callback function to handle a select field click. */ handleSelectClickCB?: () => void; /** * If `true`, the dropdown menu will be opened initially. * @default false */ openOnce?: boolean; /** * This will set the maximum width of the dropdown menu. */ menuMaxWidth?: number; /** * This will override the width of the dropdown menu. */ menuWidth?: number; } interface PaperComponentProps extends PaperProps { selectProps: MuiSelectProps & Pick; menuContentProps: SelectProps['menuContentProps']; } interface SelectBaseProps extends SelectProps { selectOpen: boolean; toggleSelect: (nextValue?: unknown) => void; } let onClickAwayFlag = false; const PaperComponent = forwardRef((props, ref) => { const { selectProps, menuContentProps, style, ...paperProps } = props; const onClickAway = useCallback(() => { onClickAwayFlag = true; }, []); const menuMaxWidth = selectProps.menuMaxWidth ? selectProps.menuMaxWidth : undefined; const menuWidth = selectProps.menuWidth ? selectProps.menuWidth : undefined; return ( ); }) as unknown as ElementType>; const SelectBase = (props: SelectBaseProps) => { const { defaultValue = '', placeholder, menuContentProps, MenuProps, label, selectOpen, menuWidth, showLimitItemsTooltip, showSelectClearButton, toggleSelect, handleSelectClickCB, onClick, classes, className, ...selectProps } = props; const [value, setValue] = useControlled({ controlled: selectProps.value, default: defaultValue, name: selectProps.id || 'select' }); const { listItems, defaultListItems, search } = menuContentProps; const { labelRoot, ...restClasses } = classes || {}; const styles = createClasses({ isValue: Array.isArray(selectProps.value) ? !!selectProps.value.length : !!selectProps.value }); const withValue = useMemo(() => listItems?.every(item => 'value' in item), [listItems]); const handleSelectClear = useCallback( (event?: React.SyntheticEvent) => { event?.stopPropagation(); menuContentProps?.clearSelectItems?.(); setValue(void 0); }, [menuContentProps, setValue] ); const getSelectedValues: (value: unknown) => ReactNode = useCallback( (value: unknown) => { const findListItem = (value: DropdownMenuItem['value'] | boolean | null) => { const listItem = listItems.find(item => item.value === value); return listItem?.textNode || listItem?.name; }; // single selection if ( (typeof value === 'string' && value) || typeof value === 'boolean' || typeof value === 'number' || value === null ) { if (withValue) { return findListItem(value); } return value; } // multiple selection if ((value as unknown[])?.length && listItems.length) { if (withValue) { return (value as DropdownMenuItem['value'][]).map(value => findListItem(value)); } return value as string[]; } return null; }, [listItems, withValue] ); const renderValue: MuiSelectProps['renderValue'] = useCallback( value => { const values = getSelectedValues(value); if (null === values) return placeholder; return ( <> {showSelectClearButton && ( )} {showLimitItemsTooltip ? ( ) : Array.isArray(values) ? ( values.join(textSeparator) ) : ( values )} ); }, [ classes, getSelectedValues, handleSelectClear, placeholder, showLimitItemsTooltip, showSelectClearButton, styles.clearButton, styles.truncatedText ] ); const handleClick = useCallback>( (name, value) => e => { setValue(value); menuContentProps.handleClick?.(name, value)(e); if (!selectProps.multiple) { toggleSelect(); } }, [menuContentProps, selectProps, setValue, toggleSelect] ); const processedListItems = useMemo(() => { const shouldBeSelected = Array.isArray(value) ? (item: DropdownMenuItem) => value.includes(item.value) : (item: DropdownMenuItem) => value === item.value; return withValue ? listItems.map(item => ({ ...item, selected: shouldBeSelected(item) })) : listItems; }, [listItems, value, withValue]); // For accessibility const handleKeyPress = useCallback( (e: KeyboardEvent) => { switch (e.code) { case 'Enter': case 'Space': e.preventDefault(); toggleSelect(); break; default: onClickAwayFlag = false; } }, [toggleSelect] ); return ( {/* This part is only for material-ui's select values support */} {menuContentProps.listItems.map(item => { const { name, value } = item; return ( ); })} ); }; const Select = (props: SelectProps) => { const { openOnce = false, ...selectBaseProps } = props; const styles = createClasses({}); const onOpenRef = useRef(props.onOpen); const [selectOpen, toggleSelect] = useToggle(openOnce); const handleSelectClick = useCallback(() => { if (!selectBaseProps.disabled) toggleSelect(); selectBaseProps.handleSelectClickCB?.(); }, [selectBaseProps, toggleSelect]); const handleLabelClick: InputLabelProps['onClick'] = useCallback( e => { e.preventDefault(); // Using a label cause the menu to stay open even after selection, this is how we prevent this behavior. if (e.target.tagName !== 'INPUT') { handleSelectClick(); } }, [handleSelectClick] ); useEffect(() => { openOnce && onOpenRef.current?.(new Event('click') as unknown as SyntheticEvent); }, [openOnce]); if (selectBaseProps.label) { return ( {selectBaseProps.label} ); } return ( ); }; const m = memo(Select); export { m as Select };