import React, { AnchorHTMLAttributes, FocusEvent, KeyboardEvent, HTMLAttributes, SyntheticEvent, createContext, Ref, FC, useContext, useRef, useMemo, ReactNode, useState, useId, } from 'react'; import classnames from 'classnames'; import { Icon } from './Icon'; import { AutoAlign, AutoAlignInjectedProps, AutoAlignProps } from './AutoAlign'; import { useEventCallback, useMergeRefs } from './hooks'; import { Bivariant } from './typeUtils'; /** * */ type EventKey = string | number; /** * */ export type DropdownMenuHeaderProps = { className?: string; divider?: 'top' | 'bottom'; children?: ReactNode; }; /** * */ export const DropdownMenuHeader: FC = ({ divider, className, children, }) => { const menuHeaderClass = classnames( 'slds-dropdown__header', 'slds-truncate', divider ? `slds-has-divider_${divider}-space` : undefined, className ); return (
  • {children}
  • ); }; export const MenuHeader = DropdownMenuHeader; /** * */ type DropdownMenuHandler = { onMenuSelect?: Bivariant<(eventKey: EventKey) => void>; onMenuFocus?: (e: FocusEvent) => void; onMenuBlur?: (e: FocusEvent) => void; }; export const DropdownMenuHandlerContext = createContext( {} ); type OpenSubmenuContext = { openSubmenuKeys: { [key: string]: { isOpen: boolean; level: number } | undefined; }; handleSubmenuOpen: (key: string, level: number) => void; }; export const OpenSubmenuContext = createContext({ openSubmenuKeys: {}, handleSubmenuOpen: () => {}, }); /** * */ export type DropdownMenuItemProps = { label?: string; eventKey?: string | number; icon?: string; iconRight?: string; disabled?: boolean; divider?: 'top' | 'bottom'; selected?: boolean; onClick?: (e: React.SyntheticEvent) => void; submenu?: ReactNode; submenuItems?: Array<{ key: string | number } & DropdownMenuItemProps>; level?: number; } & Omit, 'onClick'>; /** * */ export const DropdownMenuItem: FC = (props) => { const { className, label, icon, iconRight, selected, disabled, divider, tabIndex = 0, eventKey, onClick, onBlur, onFocus, submenu: submenu_, submenuItems, children, level = 0, ...rprops } = props; const { onMenuSelect, onMenuBlur, onMenuFocus } = useContext( DropdownMenuHandlerContext ); const { openSubmenuKeys, handleSubmenuOpen } = useContext(OpenSubmenuContext); const submenuKey = useId(); const onKeyDown = useEventCallback((e: KeyboardEvent) => { if (e.keyCode === 13 || e.keyCode === 32) { // return or space e.preventDefault(); e.stopPropagation(); onMenuItemClick(e); } else if (e.keyCode === 38 /* up */ || e.keyCode === 40 /* down */) { e.preventDefault(); e.stopPropagation(); const currentEl = e.currentTarget.parentElement; let itemEl: Element | null = currentEl ? e.keyCode === 40 ? currentEl.nextElementSibling : currentEl.previousElementSibling : null; while (itemEl) { const anchorEl = itemEl.querySelector( '.react-slds-menuitem[tabIndex]' ); if (anchorEl && !anchorEl.ariaDisabled) { anchorEl.focus(); return; } itemEl = e.keyCode === 40 ? itemEl.nextElementSibling : itemEl.previousElementSibling; } } else if ( submenuItems && (e.keyCode === 39 /* right */ || e.keyCode === 37) /* left */ ) { e.preventDefault(); e.stopPropagation(); const submenuEl = e.currentTarget.parentElement?.querySelector( '.slds-dropdown__list' ); if (submenuEl) { const anchorEl = submenuEl.querySelector( '.react-slds-menuitem[tabIndex]' ); if (anchorEl) { anchorEl.focus(); } } } }); const onMenuItemClick = useEventCallback( (e: SyntheticEvent) => { if (submenu) { handleSubmenuOpen(submenuKey, level + 1); return; } onClick?.(e); if (eventKey != null) { onMenuSelect?.(eventKey); } } ); const onMenuItemBlur = useEventCallback( (e: FocusEvent) => { onBlur?.(e); onMenuBlur?.(e); } ); const onMenuItemFocus = useEventCallback( (e: FocusEvent) => { onFocus?.(e); onMenuFocus?.(e); } ); const submenu = submenu_ ?? (submenuItems ? ( {submenuItems?.map(({ key, ...itemProps }) => ( ))} ) : undefined); const submenuExpanded = openSubmenuKeys[submenuKey]?.isOpen ?? false; const menuItemClass = classnames( 'slds-dropdown__item', { 'slds-is-selected': selected }, submenu ? 'slds-has-submenu' : undefined, className ); return ( <> {divider === 'top' ? (
  • ) : null}
  • {icon ? : null} {label || children} {iconRight || submenu ? ( ) : null} {submenu && submenuExpanded ? submenu : undefined}
  • {divider === 'bottom' ? (
  • ) : null} ); }; export const MenuItem = DropdownMenuItem; /** * */ export type DropdownSubmenuProps = { label?: string; align?: 'left' | 'right'; children?: ReactNode; }; /** * */ export const DropdownSubmenu: FC = (props) => { const { label, align = 'right', children } = props; const submenuClassName = classnames( 'slds-dropdown', 'slds-dropdown_submenu', `slds-dropdown_submenu-${align}` ); return (
      {children}
    ); }; /** * */ export type DropdownMenuProps = HTMLAttributes & { size?: 'small' | 'medium' | 'large'; header?: string; nubbin?: | 'top' | 'top left' | 'top right' | 'bottom' | 'bottom left' | 'bottom right' | 'auto'; nubbinTop?: boolean; // for backward compatibility. use nubbin instead hoverPopup?: boolean; onMenuSelect?: Bivariant<(eventKey: EventKey) => void>; onMenuClose?: () => void; elementRef?: Ref; }; /** * */ const DropdownMenuInner: FC = ( props ) => { const { className, size, header, nubbin: nubbin_, nubbinTop, hoverPopup, children, style, alignment, autoAlignContentRef, elementRef: elementRef_, onFocus, onBlur, onMenuSelect, onMenuClose, ...rprops } = props; const elRef = useRef(null); const elementRef = useMergeRefs([elRef, autoAlignContentRef, elementRef_]); const onKeyDown = useEventCallback((e: KeyboardEvent) => { if (e.keyCode === 27) { // ESC onMenuClose?.(); } }); const nubbin = nubbinTop ? 'auto' : nubbin_; const [vertAlign, align] = alignment; const nubbinPosition = nubbin === 'auto' ? alignment.join('-') : nubbin?.split(' ').join('-'); const dropdownClassNames = classnames( className, 'slds-dropdown', vertAlign ? `slds-dropdown_${vertAlign}` : undefined, align ? `slds-dropdown_${align}` : undefined, size ? `slds-dropdown_${size}` : undefined, nubbinPosition ? `slds-nubbin_${nubbinPosition}` : undefined, { 'react-slds-no-hover-popup': !hoverPopup } ); const handlers = useMemo( () => ({ onMenuSelect, onMenuBlur: onBlur, onMenuFocus: onFocus, }), [onBlur, onFocus, onMenuSelect] ); const [openSubmenuKeys, setOpenSubmenuKeys] = useState<{ [key: string]: { isOpen: boolean; level: number }; }>({}); const handleSubmenuOpen = (key: string, level: number) => { setOpenSubmenuKeys((prevState) => { const newState = { ...prevState }; Object.keys(newState).forEach((submenuKey) => { if (newState[submenuKey].level >= level && key !== submenuKey) { newState[submenuKey].isOpen = false; } }); newState[key] = { isOpen: !newState[key]?.isOpen, level }; return newState; }); }; return (
      {header ? {header} : null} {children}
    ); }; /** * */ export const DropdownMenu: FC< DropdownMenuProps & Pick > = (props) => { return ( {(injectedProps) => } ); };