Skip to content

Commit

Permalink
fix: Menu.Item not support ref (#616)
Browse files Browse the repository at this point in the history
* test: test driven

* fix: menuItem used ref
  • Loading branch information
zombieJ authored Apr 6, 2023
1 parent e196d2e commit 805be3d
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 163 deletions.
323 changes: 168 additions & 155 deletions src/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import * as React from 'react';
import classNames from 'classnames';
import Overflow from 'rc-overflow';
import warning from 'rc-util/lib/warning';
import KeyCode from 'rc-util/lib/KeyCode';
import omit from 'rc-util/lib/omit';
import type { MenuInfo, MenuItemType } from './interface';
import { useComposeRef } from 'rc-util/lib/ref';
import warning from 'rc-util/lib/warning';
import * as React from 'react';
import { useMenuId } from './context/IdContext';
import { MenuContext } from './context/MenuContext';
import useActive from './hooks/useActive';
import { warnItemProp } from './utils/warnUtil';
import Icon from './Icon';
import useDirectionStyle from './hooks/useDirectionStyle';
import { useFullPath, useMeasure } from './context/PathContext';
import { useMenuId } from './context/IdContext';
import PrivateContext from './context/PrivateContext';
import useActive from './hooks/useActive';
import useDirectionStyle from './hooks/useDirectionStyle';
import Icon from './Icon';
import type { MenuInfo, MenuItemType } from './interface';
import { warnItemProp } from './utils/warnUtil';

export interface MenuItemProps
extends Omit<MenuItemType, 'label' | 'key'>,
Expand All @@ -38,12 +39,17 @@ export interface MenuItemProps
class LegacyMenuItem extends React.Component<any> {
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']);
const passedProps = omit(restProps, [
'eventKey',
'popupClassName',
'popupOffset',
'onTitleClick',
]);
warning(
!attribute,
'`attribute` of Menu.Item is deprecated. Please pass attribute directly.',
Expand All @@ -63,184 +69,191 @@ class LegacyMenuItem extends React.Component<any> {
/**
* Real Menu Item component
*/
const InternalMenuItem = (props: MenuItemProps) => {
const {
style,
className,

eventKey,
warnKey,
disabled,
itemIcon,
children,
const InternalMenuItem = React.forwardRef(
(props: MenuItemProps, ref: React.Ref<HTMLElement>) => {
const {
style,
className,

// Aria
role,
eventKey,
warnKey,
disabled,
itemIcon,
children,

// Active
onMouseEnter,
onMouseLeave,
// Aria
role,

onClick,
onKeyDown,
// Active
onMouseEnter,
onMouseLeave,

onFocus,
onClick,
onKeyDown,

...restProps
} = props;
onFocus,

const domDataId = useMenuId(eventKey);
...restProps
} = props;

const {
prefixCls,
onItemClick,
const domDataId = useMenuId(eventKey);

disabled: contextDisabled,
overflowDisabled,
const {
prefixCls,
onItemClick,

// Icon
itemIcon: contextItemIcon,
disabled: contextDisabled,
overflowDisabled,

// Select
selectedKeys,
// Icon
itemIcon: contextItemIcon,

// Active
onActive,
} = React.useContext(MenuContext);
// Select
selectedKeys,

const { _internalRenderMenuItem } = React.useContext(PrivateContext);
// Active
onActive,
} = React.useContext(MenuContext);

const itemCls = `${prefixCls}-item`;
const { _internalRenderMenuItem } = React.useContext(PrivateContext);

const legacyMenuItemRef = React.useRef<any>();
const elementRef = React.useRef<HTMLLIElement>();
const mergedDisabled = contextDisabled || disabled;
const itemCls = `${prefixCls}-item`;

const connectedKeys = useFullPath(eventKey);

// ================================ Warn ================================
if (process.env.NODE_ENV !== 'production' && warnKey) {
warning(false, 'MenuItem should not leave undefined `key`.');
}
const legacyMenuItemRef = React.useRef<any>();
const elementRef = React.useRef<HTMLLIElement>();
const mergedDisabled = contextDisabled || disabled;

// ============================= Info =============================
const getEventInfo = (
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
): MenuInfo => {
return {
key: eventKey,
// Note: For legacy code is reversed which not like other antd component
keyPath: [...connectedKeys].reverse(),
item: legacyMenuItemRef.current,
domEvent: e,
};
};
const mergedEleRef = useComposeRef(ref, elementRef);

// ============================= Icon =============================
const mergedItemIcon = itemIcon || contextItemIcon;
const connectedKeys = useFullPath(eventKey);

// ============================ Active ============================
const { active, ...activeProps } = useActive(
eventKey,
mergedDisabled,
onMouseEnter,
onMouseLeave,
);
// ================================ Warn ================================
if (process.env.NODE_ENV !== 'production' && warnKey) {
warning(false, 'MenuItem should not leave undefined `key`.');
}

// ============================ Select ============================
const selected = selectedKeys.includes(eventKey);
// ============================= Info =============================
const getEventInfo = (
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
): MenuInfo => {
return {
key: eventKey,
// Note: For legacy code is reversed which not like other antd component
keyPath: [...connectedKeys].reverse(),
item: legacyMenuItemRef.current,
domEvent: e,
};
};

// ======================== DirectionStyle ========================
const directionStyle = useDirectionStyle(connectedKeys.length);
// ============================= Icon =============================
const mergedItemIcon = itemIcon || contextItemIcon;

// ============================ Events ============================
const onInternalClick: React.MouseEventHandler<HTMLLIElement> = e => {
if (mergedDisabled) {
return;
}
// ============================ Active ============================
const { active, ...activeProps } = useActive(
eventKey,
mergedDisabled,
onMouseEnter,
onMouseLeave,
);

const info = getEventInfo(e);
// ============================ Select ============================
const selected = selectedKeys.includes(eventKey);

onClick?.(warnItemProp(info));
onItemClick(info);
};
// ======================== DirectionStyle ========================
const directionStyle = useDirectionStyle(connectedKeys.length);

const onInternalKeyDown: React.KeyboardEventHandler<HTMLLIElement> = e => {
onKeyDown?.(e);
// ============================ Events ============================
const onInternalClick: React.MouseEventHandler<HTMLLIElement> = e => {
if (mergedDisabled) {
return;
}

if (e.which === KeyCode.ENTER) {
const info = getEventInfo(e);

// Legacy. Key will also trigger click event
onClick?.(warnItemProp(info));
onItemClick(info);
};

const onInternalKeyDown: React.KeyboardEventHandler<HTMLLIElement> = e => {
onKeyDown?.(e);

if (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<HTMLLIElement> = e => {
onActive(eventKey);
onFocus?.(e);
};

// ============================ Render ============================
const optionRoleProps: React.HTMLAttributes<HTMLDivElement> = {};

if (props.role === 'option') {
optionRoleProps['aria-selected'] = selected;
}
};

/**
* Used for accessibility. Helper will focus element without key board.
* We should manually trigger an active
*/
const onInternalFocus: React.FocusEventHandler<HTMLLIElement> = e => {
onActive(eventKey);
onFocus?.(e);
};

// ============================ Render ============================
const optionRoleProps: React.HTMLAttributes<HTMLDivElement> = {};

if (props.role === 'option') {
optionRoleProps['aria-selected'] = selected;
}

let renderNode = (
<LegacyMenuItem
ref={legacyMenuItemRef}
elementRef={elementRef}
role={role === null ? 'none' : role || 'menuitem'}
tabIndex={disabled ? null : -1}
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
{...restProps}
{...activeProps}
{...optionRoleProps}
component="li"
aria-disabled={disabled}
style={{
...directionStyle,
...style,
}}
className={classNames(
itemCls,
{
[`${itemCls}-active`]: active,
[`${itemCls}-selected`]: selected,
[`${itemCls}-disabled`]: mergedDisabled,
},
className,
)}
onClick={onInternalClick}
onKeyDown={onInternalKeyDown}
onFocus={onInternalFocus}
>
{children}
<Icon
props={{
...props,
isSelected: selected,
let renderNode = (
<LegacyMenuItem
ref={legacyMenuItemRef}
elementRef={mergedEleRef}
role={role === null ? 'none' : role || 'menuitem'}
tabIndex={disabled ? null : -1}
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
{...restProps}
{...activeProps}
{...optionRoleProps}
component="li"
aria-disabled={disabled}
style={{
...directionStyle,
...style,
}}
icon={mergedItemIcon}
/>
</LegacyMenuItem>
);
className={classNames(
itemCls,
{
[`${itemCls}-active`]: active,
[`${itemCls}-selected`]: selected,
[`${itemCls}-disabled`]: mergedDisabled,
},
className,
)}
onClick={onInternalClick}
onKeyDown={onInternalKeyDown}
onFocus={onInternalFocus}
>
{children}
<Icon
props={{
...props,
isSelected: selected,
}}
icon={mergedItemIcon}
/>
</LegacyMenuItem>
);

if (_internalRenderMenuItem) {
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
}
if (_internalRenderMenuItem) {
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
}

return renderNode;
};
return renderNode;
},
);

function MenuItem(props: MenuItemProps): React.ReactElement {
function MenuItem(
props: MenuItemProps,
ref: React.Ref<HTMLElement>,
): React.ReactElement {
const { eventKey } = props;

// ==================== Record KeyPath ====================
Expand All @@ -263,7 +276,7 @@ function MenuItem(props: MenuItemProps): React.ReactElement {
}

// ======================== Render ========================
return <InternalMenuItem {...props} />;
return <InternalMenuItem {...props} ref={ref} />;
}

export default MenuItem;
export default React.forwardRef(MenuItem);
Loading

0 comments on commit 805be3d

Please sign in to comment.