Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Menu.Item not support ref #616

Merged
merged 2 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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