import { clsx } from 'clsx'; import Overflow from '@rc-component/overflow'; import { omit, warning, KeyCode, useComposeRef } from '@rc-component/util'; import * as React from 'react'; import { useMenuId } from './context/IdContext'; import { MenuContext } from './context/MenuContext'; import { useFullPath, useMeasure } from './context/PathContext'; import PrivateContext from './context/PrivateContext'; import useActive from './hooks/useActive'; import useDirectionStyle from './hooks/useDirectionStyle'; import Icon from './Icon'; import type { MenuInfo, ItemData, MenuItemType } from './interface'; import { warnItemProp } from './utils/warnUtil'; export interface MenuItemProps extends Omit, Omit< React.HTMLAttributes, 'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onSelect' > { children?: React.ReactNode; /** @private Internal filled key. Do not set it directly */ eventKey?: string; /** @private Do not use. Private warning empty usage */ warnKey?: boolean; /** @deprecated No place to use this. Should remove */ attribute?: Record; /** @private Origin item config from items prop */ itemData?: ItemData; } // Since Menu event provide the `info.item` which point to the MenuItem node instance. // We have to use class component here. // This should be removed from doc & api in future. class LegacyMenuItem extends React.Component { render() { const { title, attribute, elementRef, ...restProps } = this.props; // Here the props are eventually passed to the DOM element. // React does not recognize non-standard attributes. // Therefore, remove the props that is not used here. // ref: https://github.com/ant-design/ant-design/issues/41395 const passedProps = omit(restProps, [ 'eventKey', 'popupClassName', 'popupOffset', 'onTitleClick', ]); warning(!attribute, '`attribute` of Menu.Item is deprecated. Please pass attribute directly.'); return ( ); } } /** * Real Menu Item component */ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref) => { const { style, className, eventKey, warnKey, disabled, itemIcon, children, itemData: propsItemData, // Aria role, // Active onMouseEnter, onMouseLeave, onClick, onKeyDown, onFocus, ...restProps } = props; const domDataId = useMenuId(eventKey); const { prefixCls, onItemClick, disabled: contextDisabled, overflowDisabled, // Icon itemIcon: contextItemIcon, // Select selectedKeys, // Active onActive, } = React.useContext(MenuContext); const { _internalRenderMenuItem } = React.useContext(PrivateContext); const itemCls = `${prefixCls}-item`; const legacyMenuItemRef = React.useRef(null); const elementRef = React.useRef(null); const mergedDisabled = contextDisabled || disabled; const mergedEleRef = useComposeRef(ref, elementRef); const connectedKeys = useFullPath(eventKey); // ================================ Warn ================================ if (process.env.NODE_ENV !== 'production' && warnKey) { warning(false, 'MenuItem should not leave undefined `key`.'); } // ============================= Info ============================= const getEventInfo = ( e: React.MouseEvent | React.KeyboardEvent, ): MenuInfo => { // If propsInfo exists (items mode), use it; otherwise build from props (children mode) const itemData: ItemData = propsItemData || { key: eventKey || '', label: children, itemIcon, extra: props.extra, title: props.title, }; return { key: eventKey, // Note: For legacy code is reversed which not like other antd component keyPath: [...connectedKeys].reverse(), item: legacyMenuItemRef.current, domEvent: e, itemData: propsItemData || itemData, }; }; // ============================= Icon ============================= const mergedItemIcon = itemIcon || contextItemIcon; // ============================ Active ============================ const { active, ...activeProps } = useActive( eventKey, mergedDisabled, onMouseEnter, onMouseLeave, ); // ============================ Select ============================ const selected = selectedKeys.includes(eventKey); // ======================== DirectionStyle ======================== const directionStyle = useDirectionStyle(connectedKeys.length); // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { if (mergedDisabled) { return; } const info = getEventInfo(e); onClick?.(warnItemProp(info)); onItemClick(info); }; const onInternalKeyDown: React.KeyboardEventHandler = e => { onKeyDown?.(e); if (!mergedDisabled && e.which === KeyCode.ENTER) { const info = getEventInfo(e); // Legacy. Key will also trigger click event onClick?.(warnItemProp(info)); onItemClick(info); } }; /** * Used for accessibility. Helper will focus element without key board. * We should manually trigger an active */ const onInternalFocus: React.FocusEventHandler = e => { onActive(eventKey); onFocus?.(e); }; // ============================ Render ============================ const optionRoleProps: React.HTMLAttributes = {}; if (props.role === 'option') { optionRoleProps['aria-selected'] = selected; } let renderNode = ( {children} ); if (_internalRenderMenuItem) { renderNode = _internalRenderMenuItem(renderNode, props, { selected }); } return renderNode; }); function MenuItem(props: MenuItemProps, ref: React.Ref): React.ReactElement { const { eventKey } = props; // ==================== Record KeyPath ==================== const measure = useMeasure(); const connectedKeyPath = useFullPath(eventKey); // eslint-disable-next-line consistent-return React.useEffect(() => { if (measure) { measure.registerPath(eventKey, connectedKeyPath); return () => { measure.unregisterPath(eventKey, connectedKeyPath); }; } }, [connectedKeyPath]); if (measure) { return null; } // ======================== Render ======================== return ; } export default React.forwardRef(MenuItem);