Skip to content

Commit 805be3d

Browse files
authored
fix: Menu.Item not support ref (#616)
* test: test driven * fix: menuItem used ref
1 parent e196d2e commit 805be3d

File tree

3 files changed

+209
-163
lines changed

3 files changed

+209
-163
lines changed

src/MenuItem.tsx

Lines changed: 168 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import * as React from 'react';
21
import classNames from 'classnames';
32
import Overflow from 'rc-overflow';
4-
import warning from 'rc-util/lib/warning';
53
import KeyCode from 'rc-util/lib/KeyCode';
64
import omit from 'rc-util/lib/omit';
7-
import type { MenuInfo, MenuItemType } from './interface';
5+
import { useComposeRef } from 'rc-util/lib/ref';
6+
import warning from 'rc-util/lib/warning';
7+
import * as React from 'react';
8+
import { useMenuId } from './context/IdContext';
89
import { MenuContext } from './context/MenuContext';
9-
import useActive from './hooks/useActive';
10-
import { warnItemProp } from './utils/warnUtil';
11-
import Icon from './Icon';
12-
import useDirectionStyle from './hooks/useDirectionStyle';
1310
import { useFullPath, useMeasure } from './context/PathContext';
14-
import { useMenuId } from './context/IdContext';
1511
import PrivateContext from './context/PrivateContext';
12+
import useActive from './hooks/useActive';
13+
import useDirectionStyle from './hooks/useDirectionStyle';
14+
import Icon from './Icon';
15+
import type { MenuInfo, MenuItemType } from './interface';
16+
import { warnItemProp } from './utils/warnUtil';
1617

1718
export interface MenuItemProps
1819
extends Omit<MenuItemType, 'label' | 'key'>,
@@ -38,12 +39,17 @@ export interface MenuItemProps
3839
class LegacyMenuItem extends React.Component<any> {
3940
render() {
4041
const { title, attribute, elementRef, ...restProps } = this.props;
41-
42+
4243
// Here the props are eventually passed to the DOM element.
4344
// React does not recognize non-standard attributes.
4445
// Therefore, remove the props that is not used here.
4546
// ref: https://github.com/ant-design/ant-design/issues/41395
46-
const passedProps = omit(restProps, ['eventKey', 'popupClassName', 'popupOffset', 'onTitleClick']);
47+
const passedProps = omit(restProps, [
48+
'eventKey',
49+
'popupClassName',
50+
'popupOffset',
51+
'onTitleClick',
52+
]);
4753
warning(
4854
!attribute,
4955
'`attribute` of Menu.Item is deprecated. Please pass attribute directly.',
@@ -63,184 +69,191 @@ class LegacyMenuItem extends React.Component<any> {
6369
/**
6470
* Real Menu Item component
6571
*/
66-
const InternalMenuItem = (props: MenuItemProps) => {
67-
const {
68-
style,
69-
className,
70-
71-
eventKey,
72-
warnKey,
73-
disabled,
74-
itemIcon,
75-
children,
72+
const InternalMenuItem = React.forwardRef(
73+
(props: MenuItemProps, ref: React.Ref<HTMLElement>) => {
74+
const {
75+
style,
76+
className,
7677

77-
// Aria
78-
role,
78+
eventKey,
79+
warnKey,
80+
disabled,
81+
itemIcon,
82+
children,
7983

80-
// Active
81-
onMouseEnter,
82-
onMouseLeave,
84+
// Aria
85+
role,
8386

84-
onClick,
85-
onKeyDown,
87+
// Active
88+
onMouseEnter,
89+
onMouseLeave,
8690

87-
onFocus,
91+
onClick,
92+
onKeyDown,
8893

89-
...restProps
90-
} = props;
94+
onFocus,
9195

92-
const domDataId = useMenuId(eventKey);
96+
...restProps
97+
} = props;
9398

94-
const {
95-
prefixCls,
96-
onItemClick,
99+
const domDataId = useMenuId(eventKey);
97100

98-
disabled: contextDisabled,
99-
overflowDisabled,
101+
const {
102+
prefixCls,
103+
onItemClick,
100104

101-
// Icon
102-
itemIcon: contextItemIcon,
105+
disabled: contextDisabled,
106+
overflowDisabled,
103107

104-
// Select
105-
selectedKeys,
108+
// Icon
109+
itemIcon: contextItemIcon,
106110

107-
// Active
108-
onActive,
109-
} = React.useContext(MenuContext);
111+
// Select
112+
selectedKeys,
110113

111-
const { _internalRenderMenuItem } = React.useContext(PrivateContext);
114+
// Active
115+
onActive,
116+
} = React.useContext(MenuContext);
112117

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

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

119-
const connectedKeys = useFullPath(eventKey);
120-
121-
// ================================ Warn ================================
122-
if (process.env.NODE_ENV !== 'production' && warnKey) {
123-
warning(false, 'MenuItem should not leave undefined `key`.');
124-
}
122+
const legacyMenuItemRef = React.useRef<any>();
123+
const elementRef = React.useRef<HTMLLIElement>();
124+
const mergedDisabled = contextDisabled || disabled;
125125

126-
// ============================= Info =============================
127-
const getEventInfo = (
128-
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
129-
): MenuInfo => {
130-
return {
131-
key: eventKey,
132-
// Note: For legacy code is reversed which not like other antd component
133-
keyPath: [...connectedKeys].reverse(),
134-
item: legacyMenuItemRef.current,
135-
domEvent: e,
136-
};
137-
};
126+
const mergedEleRef = useComposeRef(ref, elementRef);
138127

139-
// ============================= Icon =============================
140-
const mergedItemIcon = itemIcon || contextItemIcon;
128+
const connectedKeys = useFullPath(eventKey);
141129

142-
// ============================ Active ============================
143-
const { active, ...activeProps } = useActive(
144-
eventKey,
145-
mergedDisabled,
146-
onMouseEnter,
147-
onMouseLeave,
148-
);
130+
// ================================ Warn ================================
131+
if (process.env.NODE_ENV !== 'production' && warnKey) {
132+
warning(false, 'MenuItem should not leave undefined `key`.');
133+
}
149134

150-
// ============================ Select ============================
151-
const selected = selectedKeys.includes(eventKey);
135+
// ============================= Info =============================
136+
const getEventInfo = (
137+
e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
138+
): MenuInfo => {
139+
return {
140+
key: eventKey,
141+
// Note: For legacy code is reversed which not like other antd component
142+
keyPath: [...connectedKeys].reverse(),
143+
item: legacyMenuItemRef.current,
144+
domEvent: e,
145+
};
146+
};
152147

153-
// ======================== DirectionStyle ========================
154-
const directionStyle = useDirectionStyle(connectedKeys.length);
148+
// ============================= Icon =============================
149+
const mergedItemIcon = itemIcon || contextItemIcon;
155150

156-
// ============================ Events ============================
157-
const onInternalClick: React.MouseEventHandler<HTMLLIElement> = e => {
158-
if (mergedDisabled) {
159-
return;
160-
}
151+
// ============================ Active ============================
152+
const { active, ...activeProps } = useActive(
153+
eventKey,
154+
mergedDisabled,
155+
onMouseEnter,
156+
onMouseLeave,
157+
);
161158

162-
const info = getEventInfo(e);
159+
// ============================ Select ============================
160+
const selected = selectedKeys.includes(eventKey);
163161

164-
onClick?.(warnItemProp(info));
165-
onItemClick(info);
166-
};
162+
// ======================== DirectionStyle ========================
163+
const directionStyle = useDirectionStyle(connectedKeys.length);
167164

168-
const onInternalKeyDown: React.KeyboardEventHandler<HTMLLIElement> = e => {
169-
onKeyDown?.(e);
165+
// ============================ Events ============================
166+
const onInternalClick: React.MouseEventHandler<HTMLLIElement> = e => {
167+
if (mergedDisabled) {
168+
return;
169+
}
170170

171-
if (e.which === KeyCode.ENTER) {
172171
const info = getEventInfo(e);
173172

174-
// Legacy. Key will also trigger click event
175173
onClick?.(warnItemProp(info));
176174
onItemClick(info);
175+
};
176+
177+
const onInternalKeyDown: React.KeyboardEventHandler<HTMLLIElement> = e => {
178+
onKeyDown?.(e);
179+
180+
if (e.which === KeyCode.ENTER) {
181+
const info = getEventInfo(e);
182+
183+
// Legacy. Key will also trigger click event
184+
onClick?.(warnItemProp(info));
185+
onItemClick(info);
186+
}
187+
};
188+
189+
/**
190+
* Used for accessibility. Helper will focus element without key board.
191+
* We should manually trigger an active
192+
*/
193+
const onInternalFocus: React.FocusEventHandler<HTMLLIElement> = e => {
194+
onActive(eventKey);
195+
onFocus?.(e);
196+
};
197+
198+
// ============================ Render ============================
199+
const optionRoleProps: React.HTMLAttributes<HTMLDivElement> = {};
200+
201+
if (props.role === 'option') {
202+
optionRoleProps['aria-selected'] = selected;
177203
}
178-
};
179-
180-
/**
181-
* Used for accessibility. Helper will focus element without key board.
182-
* We should manually trigger an active
183-
*/
184-
const onInternalFocus: React.FocusEventHandler<HTMLLIElement> = e => {
185-
onActive(eventKey);
186-
onFocus?.(e);
187-
};
188-
189-
// ============================ Render ============================
190-
const optionRoleProps: React.HTMLAttributes<HTMLDivElement> = {};
191-
192-
if (props.role === 'option') {
193-
optionRoleProps['aria-selected'] = selected;
194-
}
195204

196-
let renderNode = (
197-
<LegacyMenuItem
198-
ref={legacyMenuItemRef}
199-
elementRef={elementRef}
200-
role={role === null ? 'none' : role || 'menuitem'}
201-
tabIndex={disabled ? null : -1}
202-
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
203-
{...restProps}
204-
{...activeProps}
205-
{...optionRoleProps}
206-
component="li"
207-
aria-disabled={disabled}
208-
style={{
209-
...directionStyle,
210-
...style,
211-
}}
212-
className={classNames(
213-
itemCls,
214-
{
215-
[`${itemCls}-active`]: active,
216-
[`${itemCls}-selected`]: selected,
217-
[`${itemCls}-disabled`]: mergedDisabled,
218-
},
219-
className,
220-
)}
221-
onClick={onInternalClick}
222-
onKeyDown={onInternalKeyDown}
223-
onFocus={onInternalFocus}
224-
>
225-
{children}
226-
<Icon
227-
props={{
228-
...props,
229-
isSelected: selected,
205+
let renderNode = (
206+
<LegacyMenuItem
207+
ref={legacyMenuItemRef}
208+
elementRef={mergedEleRef}
209+
role={role === null ? 'none' : role || 'menuitem'}
210+
tabIndex={disabled ? null : -1}
211+
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
212+
{...restProps}
213+
{...activeProps}
214+
{...optionRoleProps}
215+
component="li"
216+
aria-disabled={disabled}
217+
style={{
218+
...directionStyle,
219+
...style,
230220
}}
231-
icon={mergedItemIcon}
232-
/>
233-
</LegacyMenuItem>
234-
);
221+
className={classNames(
222+
itemCls,
223+
{
224+
[`${itemCls}-active`]: active,
225+
[`${itemCls}-selected`]: selected,
226+
[`${itemCls}-disabled`]: mergedDisabled,
227+
},
228+
className,
229+
)}
230+
onClick={onInternalClick}
231+
onKeyDown={onInternalKeyDown}
232+
onFocus={onInternalFocus}
233+
>
234+
{children}
235+
<Icon
236+
props={{
237+
...props,
238+
isSelected: selected,
239+
}}
240+
icon={mergedItemIcon}
241+
/>
242+
</LegacyMenuItem>
243+
);
235244

236-
if (_internalRenderMenuItem) {
237-
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
238-
}
245+
if (_internalRenderMenuItem) {
246+
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
247+
}
239248

240-
return renderNode;
241-
};
249+
return renderNode;
250+
},
251+
);
242252

243-
function MenuItem(props: MenuItemProps): React.ReactElement {
253+
function MenuItem(
254+
props: MenuItemProps,
255+
ref: React.Ref<HTMLElement>,
256+
): React.ReactElement {
244257
const { eventKey } = props;
245258

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

265278
// ======================== Render ========================
266-
return <InternalMenuItem {...props} />;
279+
return <InternalMenuItem {...props} ref={ref} />;
267280
}
268281

269-
export default MenuItem;
282+
export default React.forwardRef(MenuItem);

0 commit comments

Comments
 (0)