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