diff --git a/README.md b/README.md
index 16b5278f..9f29d3d8 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,12 @@ ReactDOM.render(
+
+ ref |
+ React.HTMLLIElement |
+ |
+ get dom node |
+
className |
String |
@@ -294,6 +300,12 @@ ReactDOM.render(
+
+ ref |
+ React.HTMLLIElement |
+ |
+ get dom node |
+
popupClassName |
String |
@@ -397,6 +409,12 @@ none
+
+ ref |
+ React.HTMLLIElement |
+ |
+ get dom node |
+
title |
String|React.Element |
diff --git a/docs/demo/items-ref.md b/docs/demo/items-ref.md
new file mode 100644
index 00000000..62e9fefa
--- /dev/null
+++ b/docs/demo/items-ref.md
@@ -0,0 +1,3 @@
+## items-ref
+
+
diff --git a/docs/examples/items-ref.tsx b/docs/examples/items-ref.tsx
new file mode 100644
index 00000000..dc18f9f1
--- /dev/null
+++ b/docs/examples/items-ref.tsx
@@ -0,0 +1,100 @@
+/* eslint no-console:0 */
+
+import React, { useRef } from 'react';
+import '../../assets/index.less';
+import Menu from '../../src';
+
+export default () => {
+ const ref1 = useRef();
+ const ref2 = useRef();
+ const ref3 = useRef();
+ const ref4 = useRef();
+ const ref5 = useRef();
+ const ref6 = useRef();
+ const ref7 = useRef();
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx
index 1be29261..284747a3 100644
--- a/src/MenuItem.tsx
+++ b/src/MenuItem.tsx
@@ -16,7 +16,7 @@ import type { MenuInfo, MenuItemType } from './interface';
import { warnItemProp } from './utils/warnUtil';
export interface MenuItemProps
- extends Omit,
+ extends Omit,
Omit<
React.HTMLAttributes,
'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onSelect'
diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx
index f812c0b6..c41bd7f3 100644
--- a/src/MenuItemGroup.tsx
+++ b/src/MenuItemGroup.tsx
@@ -19,19 +19,18 @@ export interface MenuItemGroupProps
warnKey?: boolean;
}
-const InternalMenuItemGroup = ({
- className,
- title,
- eventKey,
- children,
- ...restProps
-}: MenuItemGroupProps) => {
+const InternalMenuItemGroup = React.forwardRef<
+ HTMLLIElement,
+ MenuItemGroupProps
+>((props, ref) => {
+ const { className, title, eventKey, children, ...restProps } = props;
const { prefixCls } = React.useContext(MenuContext);
const groupPrefixCls = `${prefixCls}-item-group`;
return (
e.stopPropagation()}
@@ -49,26 +48,32 @@ const InternalMenuItemGroup = ({
);
-};
+});
-export default function MenuItemGroup({
- children,
- ...props
-}: MenuItemGroupProps): React.ReactElement {
- const connectedKeyPath = useFullPath(props.eventKey);
- const childList: React.ReactElement[] = parseChildren(
- children,
- connectedKeyPath,
- );
+const MenuItemGroup = React.forwardRef(
+ (props, ref) => {
+ const { eventKey, children } = props;
+ const connectedKeyPath = useFullPath(eventKey);
+ const childList: React.ReactElement[] = parseChildren(
+ children,
+ connectedKeyPath,
+ );
- const measure = useMeasure();
- if (measure) {
- return (childList as any) as React.ReactElement;
- }
+ const measure = useMeasure();
+ if (measure) {
+ return childList as any as React.ReactElement;
+ }
- return (
-
- {childList}
-
- );
+ return (
+
+ {childList}
+
+ );
+ },
+);
+
+if (process.env.NODE_ENV !== 'production') {
+ MenuItemGroup.displayName = 'MenuItemGroup';
}
+
+export default MenuItemGroup;
diff --git a/src/SubMenu/index.tsx b/src/SubMenu/index.tsx
index 06183bd2..fc962f75 100644
--- a/src/SubMenu/index.tsx
+++ b/src/SubMenu/index.tsx
@@ -41,213 +41,214 @@ export interface SubMenuProps
// onDestroy?: DestroyEventHandler;
}
-const InternalSubMenu = (props: SubMenuProps) => {
- const {
- style,
- className,
+const InternalSubMenu = React.forwardRef(
+ (props, ref) => {
+ const {
+ style,
+ className,
- title,
- eventKey,
- warnKey,
+ title,
+ eventKey,
+ warnKey,
- disabled,
- internalPopupClose,
+ disabled,
+ internalPopupClose,
- children,
-
- // Icons
- itemIcon,
- expandIcon,
-
- // Popup
- popupClassName,
- popupOffset,
- popupStyle,
-
- // Events
- onClick,
- onMouseEnter,
- onMouseLeave,
- onTitleClick,
- onTitleMouseEnter,
- onTitleMouseLeave,
-
- ...restProps
- } = props;
-
- const domDataId = useMenuId(eventKey);
+ children,
- const {
- prefixCls,
- mode,
- openKeys,
+ // Icons
+ itemIcon,
+ expandIcon,
- // Disabled
- disabled: contextDisabled,
- overflowDisabled,
+ // Popup
+ popupClassName,
+ popupOffset,
+ popupStyle,
- // ActiveKey
- activeKey,
+ // Events
+ onClick,
+ onMouseEnter,
+ onMouseLeave,
+ onTitleClick,
+ onTitleMouseEnter,
+ onTitleMouseLeave,
- // SelectKey
- selectedKeys,
+ ...restProps
+ } = props;
- // Icon
- itemIcon: contextItemIcon,
- expandIcon: contextExpandIcon,
+ const domDataId = useMenuId(eventKey);
- // Events
- onItemClick,
- onOpenChange,
+ const {
+ prefixCls,
+ mode,
+ openKeys,
- onActive,
- } = React.useContext(MenuContext);
+ // Disabled
+ disabled: contextDisabled,
+ overflowDisabled,
- const { _internalRenderSubMenuItem } = React.useContext(PrivateContext);
+ // ActiveKey
+ activeKey,
- const { isSubPathKey } = React.useContext(PathUserContext);
- const connectedPath = useFullPath();
+ // SelectKey
+ selectedKeys,
- const subMenuPrefixCls = `${prefixCls}-submenu`;
- const mergedDisabled = contextDisabled || disabled;
- const elementRef = React.useRef();
- const popupRef = React.useRef();
+ // Icon
+ itemIcon: contextItemIcon,
+ expandIcon: contextExpandIcon,
- // ================================ Warn ================================
- if (process.env.NODE_ENV !== 'production' && warnKey) {
- warning(false, 'SubMenu should not leave undefined `key`.');
- }
-
- // ================================ Icon ================================
- const mergedItemIcon = itemIcon ?? contextItemIcon;
- const mergedExpandIcon = expandIcon ?? contextExpandIcon;
+ // Events
+ onItemClick,
+ onOpenChange,
- // ================================ Open ================================
- const originOpen = openKeys.includes(eventKey);
- const open = !overflowDisabled && originOpen;
+ onActive,
+ } = React.useContext(MenuContext);
- // =============================== Select ===============================
- const childrenSelected = isSubPathKey(selectedKeys, eventKey);
+ const { _internalRenderSubMenuItem } = React.useContext(PrivateContext);
- // =============================== Active ===============================
- const { active, ...activeProps } = useActive(
- eventKey,
- mergedDisabled,
- onTitleMouseEnter,
- onTitleMouseLeave,
- );
+ const { isSubPathKey } = React.useContext(PathUserContext);
+ const connectedPath = useFullPath();
- // Fallback of active check to avoid hover on menu title or disabled item
- const [childrenActive, setChildrenActive] = React.useState(false);
+ const subMenuPrefixCls = `${prefixCls}-submenu`;
+ const mergedDisabled = contextDisabled || disabled;
+ const elementRef = React.useRef();
+ const popupRef = React.useRef();
- const triggerChildrenActive = (newActive: boolean) => {
- if (!mergedDisabled) {
- setChildrenActive(newActive);
+ // ================================ Warn ================================
+ if (process.env.NODE_ENV !== 'production' && warnKey) {
+ warning(false, 'SubMenu should not leave undefined `key`.');
}
- };
- const onInternalMouseEnter: React.MouseEventHandler<
- HTMLLIElement
- > = domEvent => {
- triggerChildrenActive(true);
+ // ================================ Icon ================================
+ const mergedItemIcon = itemIcon ?? contextItemIcon;
+ const mergedExpandIcon = expandIcon ?? contextExpandIcon;
- onMouseEnter?.({
- key: eventKey,
- domEvent,
- });
- };
-
- const onInternalMouseLeave: React.MouseEventHandler<
- HTMLLIElement
- > = domEvent => {
- triggerChildrenActive(false);
+ // ================================ Open ================================
+ const originOpen = openKeys.includes(eventKey);
+ const open = !overflowDisabled && originOpen;
- onMouseLeave?.({
- key: eventKey,
- domEvent,
- });
- };
+ // =============================== Select ===============================
+ const childrenSelected = isSubPathKey(selectedKeys, eventKey);
- const mergedActive = React.useMemo(() => {
- if (active) {
- return active;
- }
-
- if (mode !== 'inline') {
- return childrenActive || isSubPathKey([activeKey], eventKey);
- }
-
- return false;
- }, [mode, active, activeKey, childrenActive, eventKey, isSubPathKey]);
-
- // ========================== DirectionStyle ==========================
- const directionStyle = useDirectionStyle(connectedPath.length);
-
- // =============================== Events ===============================
- // >>>> Title click
- const onInternalTitleClick: React.MouseEventHandler = e => {
- // Skip if disabled
- if (mergedDisabled) {
- return;
- }
+ // =============================== Active ===============================
+ const { active, ...activeProps } = useActive(
+ eventKey,
+ mergedDisabled,
+ onTitleMouseEnter,
+ onTitleMouseLeave,
+ );
- onTitleClick?.({
- key: eventKey,
- domEvent: e,
+ // Fallback of active check to avoid hover on menu title or disabled item
+ const [childrenActive, setChildrenActive] = React.useState(false);
+
+ const triggerChildrenActive = (newActive: boolean) => {
+ if (!mergedDisabled) {
+ setChildrenActive(newActive);
+ }
+ };
+
+ const onInternalMouseEnter: React.MouseEventHandler<
+ HTMLLIElement
+ > = domEvent => {
+ triggerChildrenActive(true);
+
+ onMouseEnter?.({
+ key: eventKey,
+ domEvent,
+ });
+ };
+
+ const onInternalMouseLeave: React.MouseEventHandler<
+ HTMLLIElement
+ > = domEvent => {
+ triggerChildrenActive(false);
+
+ onMouseLeave?.({
+ key: eventKey,
+ domEvent,
+ });
+ };
+
+ const mergedActive = React.useMemo(() => {
+ if (active) {
+ return active;
+ }
+
+ if (mode !== 'inline') {
+ return childrenActive || isSubPathKey([activeKey], eventKey);
+ }
+
+ return false;
+ }, [mode, active, activeKey, childrenActive, eventKey, isSubPathKey]);
+
+ // ========================== DirectionStyle ==========================
+ const directionStyle = useDirectionStyle(connectedPath.length);
+
+ // =============================== Events ===============================
+ // >>>> Title click
+ const onInternalTitleClick: React.MouseEventHandler = e => {
+ // Skip if disabled
+ if (mergedDisabled) {
+ return;
+ }
+
+ onTitleClick?.({
+ key: eventKey,
+ domEvent: e,
+ });
+
+ // Trigger open by click when mode is `inline`
+ if (mode === 'inline') {
+ onOpenChange(eventKey, !originOpen);
+ }
+ };
+
+ // >>>> Context for children click
+ const onMergedItemClick = useMemoCallback((info: MenuInfo) => {
+ onClick?.(warnItemProp(info));
+ onItemClick(info);
});
- // Trigger open by click when mode is `inline`
- if (mode === 'inline') {
- onOpenChange(eventKey, !originOpen);
- }
- };
-
- // >>>> Context for children click
- const onMergedItemClick = useMemoCallback((info: MenuInfo) => {
- onClick?.(warnItemProp(info));
- onItemClick(info);
- });
-
- // >>>>> Visible change
- const onPopupVisibleChange = (newVisible: boolean) => {
- if (mode !== 'inline') {
- onOpenChange(eventKey, newVisible);
- }
- };
-
- /**
- * Used for accessibility. Helper will focus element without key board.
- * We should manually trigger an active
- */
- const onInternalFocus: React.FocusEventHandler = () => {
- onActive(eventKey);
- };
-
- // =============================== Render ===============================
- const popupId = domDataId && `${domDataId}-popup`;
-
- // >>>>> Title
- let titleNode: React.ReactElement = (
-
- {title}
-
- {/* Only non-horizontal mode shows the icon */}
-
>>>> Visible change
+ const onPopupVisibleChange = (newVisible: boolean) => {
+ if (mode !== 'inline') {
+ onOpenChange(eventKey, newVisible);
+ }
+ };
+
+ /**
+ * Used for accessibility. Helper will focus element without key board.
+ * We should manually trigger an active
+ */
+ const onInternalFocus: React.FocusEventHandler = () => {
+ onActive(eventKey);
+ };
+
+ // =============================== Render ===============================
+ const popupId = domDataId && `${domDataId}-popup`;
+
+ // >>>>> Title
+ let titleNode: React.ReactElement = (
+
+ {title}
+
+ {/* Only non-horizontal mode shows the icon */}
+ {
>
+
+ );
-
- );
+ // Cache mode if it change to `inline` which do not have popup motion
+ const triggerModeRef = React.useRef(mode);
+ if (mode !== 'inline' && connectedPath.length > 1) {
+ triggerModeRef.current = 'vertical';
+ } else {
+ triggerModeRef.current = mode;
+ }
- // Cache mode if it change to `inline` which do not have popup motion
- const triggerModeRef = React.useRef(mode);
- if (mode !== 'inline' && connectedPath.length > 1) {
- triggerModeRef.current = 'vertical';
- } else {
- triggerModeRef.current = mode;
- }
+ if (!overflowDisabled) {
+ const triggerMode = triggerModeRef.current;
+
+ // Still wrap with Trigger here since we need avoid react re-mount dom node
+ // Which makes motion failed
+ titleNode = (
+
+
+
+ }
+ disabled={mergedDisabled}
+ onVisibleChange={onPopupVisibleChange}
+ >
+ {titleNode}
+
+ );
+ }
- if (!overflowDisabled) {
- const triggerMode = triggerModeRef.current;
-
- // Still wrap with Trigger here since we need avoid react re-mount dom node
- // Which makes motion failed
- titleNode = (
-
-
-
- }
- disabled={mergedDisabled}
- onVisibleChange={onPopupVisibleChange}
+ // >>>>> List node
+ let listNode = (
+
{titleNode}
-
- );
- }
- // >>>>> List node
- let listNode = (
-
- {titleNode}
-
- {/* Inline mode */}
- {!overflowDisabled && (
-
- )}
-
- );
+ {/* Inline mode */}
+ {!overflowDisabled && (
+
+ )}
+
+ );
- if (_internalRenderSubMenuItem) {
- listNode = _internalRenderSubMenuItem(listNode, props, {
- selected: childrenSelected,
- active: mergedActive,
- open,
- disabled: mergedDisabled,
- });
- }
+ if (_internalRenderSubMenuItem) {
+ listNode = _internalRenderSubMenuItem(listNode, props, {
+ selected: childrenSelected,
+ active: mergedActive,
+ open,
+ disabled: mergedDisabled,
+ });
+ }
- // >>>>> Render
- return (
-
- {listNode}
-
- );
-};
+ // >>>>> Render
+ return (
+
+ {listNode}
+
+ );
+ },
+);
-export default function SubMenu(props: SubMenuProps) {
+const SubMenu = React.forwardRef((props, ref) => {
const { eventKey, children } = props;
const connectedKeyPath = useFullPath(eventKey);
@@ -384,7 +386,11 @@ export default function SubMenu(props: SubMenuProps) {
if (measure) {
renderNode = childList;
} else {
- renderNode = {childList};
+ renderNode = (
+
+ {childList}
+
+ );
}
return (
@@ -392,4 +398,10 @@ export default function SubMenu(props: SubMenuProps) {
{renderNode}
);
+});
+
+if (process.env.NODE_ENV !== 'production') {
+ SubMenu.displayName = 'SubMenu';
}
+
+export default SubMenu;
diff --git a/src/interface.ts b/src/interface.ts
index 5abc3f3c..108632dc 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -2,6 +2,7 @@ import type * as React from 'react';
// ========================= Options =========================
interface ItemSharedProps {
+ ref?: React.Ref;
style?: React.CSSProperties;
className?: string;
}
@@ -66,7 +67,7 @@ export interface MenuItemGroupType extends ItemSharedProps {
children?: ItemType[];
}
-export interface MenuDividerType extends ItemSharedProps {
+export interface MenuDividerType extends Omit {
type: 'divider';
}
diff --git a/tests/Menu.spec.tsx b/tests/Menu.spec.tsx
index 840f6f0b..fc7326ef 100644
--- a/tests/Menu.spec.tsx
+++ b/tests/Menu.spec.tsx
@@ -225,6 +225,54 @@ describe('Menu', () => {
);
});
+ it('items support ref', () => {
+ const ref1 = React.createRef();
+ const ref2 = React.createRef();
+ const ref3 = React.createRef();
+ const ref4 = React.createRef();
+
+ render(
+ ,
+ );
+
+ expect(ref1.current.innerHTML).toBeTruthy();
+ expect(ref2.current.innerHTML).toBeTruthy();
+ expect(ref3.current.innerHTML).toBeTruthy();
+ expect(ref4.current.innerHTML).toBeTruthy();
+ });
+
it('can be controlled by selectedKeys', () => {
const genMenu = (props?) => (