diff --git a/README.md b/README.md index 13de7032..ae8eeaed 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,12 @@ ReactDOM.render(
\ No newline at end of file
diff --git a/docs/examples/antd-switch-multi.tsx b/docs/examples/antd-switch-multi.tsx
new file mode 100644
index 00000000..e4697e45
--- /dev/null
+++ b/docs/examples/antd-switch-multi.tsx
@@ -0,0 +1,44 @@
+/* eslint-disable no-console, react/require-default-props, no-param-reassign */
+
+import React from 'react';
+import { CommonMenu, inlineMotion } from './antd';
+import '../../assets/index.less';
+
+const Demo = () => {
+ const [inline, setInline] = React.useState(false);
+ const [openKeys, setOpenKey] = React.useState(['1']);
+
+ let restProps = {};
+ if (inline) {
+ restProps = { motion: inlineMotion };
+ } else {
+ restProps = { openAnimation: 'zoom' };
+ }
+
+ return (
+
+
+ {
+ console.error('Open Keys Changed:', keys);
+ setOpenKey(keys);
+ }}
+ inlineCollapsed={!inline}
+ {...restProps}
+ />
+
+ );
+};
+
+export default Demo;
+/* eslint-enable */
diff --git a/src/Menu.tsx b/src/Menu.tsx
index 69e271ed..0be2b1cb 100644
--- a/src/Menu.tsx
+++ b/src/Menu.tsx
@@ -20,6 +20,7 @@ import { parseChildren } from './utils/nodeUtil';
import MenuContextProvider from './context/MenuContext';
import useMemoCallback from './hooks/useMemoCallback';
import { warnItemProp } from './utils/warnUtil';
+import { genMultiMode } from './utils/multiModeUtil';
import SubMenu from './SubMenu';
import useAccessibility from './hooks/useAccessibility';
import useUUID from './hooks/useUUID';
@@ -61,6 +62,7 @@ export interface MenuProps
// Mode
mode?: MenuMode;
+ inlineMaxDeep?: number;
inlineCollapsed?: boolean;
// Open control
@@ -130,6 +132,7 @@ const Menu: React.FC = props => {
// Mode
mode = 'vertical',
+ inlineMaxDeep,
inlineCollapsed,
// Disabled
@@ -227,14 +230,17 @@ const Menu: React.FC = props => {
postState: keys => keys || EMPTY_LIST,
});
+ const isMultiMode = genMultiMode(mergedOpenKeys, mergedMode, inlineMaxDeep);
+
const triggerOpenKeys = (keys: string[]) => {
setMergedOpenKeys(keys);
onOpenChange?.(keys);
};
// >>>>> Cache & Reset open keys when inlineCollapsed changed
- const [inlineCacheOpenKeys, setInlineCacheOpenKeys] =
- React.useState(mergedOpenKeys);
+ const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = React.useState(
+ mergedOpenKeys,
+ );
const isInlineMode = mergedMode === 'inline';
@@ -279,10 +285,9 @@ const Menu: React.FC = props => {
[registerPath, unregisterPath],
);
- const pathUserContext = React.useMemo(
- () => ({ isSubPathKey }),
- [isSubPathKey],
- );
+ const pathUserContext = React.useMemo(() => ({ isSubPathKey }), [
+ isSubPathKey,
+ ]);
React.useEffect(() => {
refreshOverflowKeys(
@@ -369,6 +374,18 @@ const Menu: React.FC = props => {
if (!multiple && mergedOpenKeys.length && mergedMode !== 'inline') {
triggerOpenKeys(EMPTY_LIST);
}
+
+ if (!multiple && isMultiMode.isMultiPopup) {
+ const inlineLevelPathKeys =
+ info.keyPath[info.keyPath.length - inlineMaxDeep + 1];
+ if (inlineLevelPathKeys) {
+ const subPathKeys = getSubPathKeys(inlineLevelPathKeys);
+ const newOpenKeys = mergedOpenKeys.filter(k => !subPathKeys.has(k));
+ triggerOpenKeys(newOpenKeys);
+ } else {
+ triggerOpenKeys(EMPTY_LIST);
+ }
+ }
};
// ========================= Open =========================
@@ -385,7 +402,7 @@ const Menu: React.FC = props => {
if (open) {
newOpenKeys.push(key);
- } else if (mergedMode !== 'inline') {
+ } else if (mergedMode !== 'inline' || isMultiMode.isMultiPopup) {
// We need find all related popup to close
const subPathKeys = getSubPathKeys(key);
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.has(k));
@@ -506,6 +523,7 @@ const Menu: React.FC = props => {
{
const {
prefixCls,
mode,
+ inlineMaxDeep,
openKeys,
// Disabled
@@ -129,6 +131,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
const { isSubPathKey } = React.useContext(PathUserContext);
const connectedPath = useFullPath();
+ const isMultiMode = genMultiMode(connectedPath, mode, inlineMaxDeep);
const subMenuPrefixCls = `${prefixCls}-submenu`;
const mergedDisabled = contextDisabled || disabled;
@@ -168,25 +171,23 @@ const InternalSubMenu = (props: SubMenuProps) => {
}
};
- const onInternalMouseEnter: React.MouseEventHandler =
- domEvent => {
- triggerChildrenActive(true);
+ const onInternalMouseEnter: React.MouseEventHandler = domEvent => {
+ triggerChildrenActive(true);
- onMouseEnter?.({
- key: eventKey,
- domEvent,
- });
- };
+ onMouseEnter?.({
+ key: eventKey,
+ domEvent,
+ });
+ };
- const onInternalMouseLeave: React.MouseEventHandler =
- domEvent => {
- triggerChildrenActive(false);
+ const onInternalMouseLeave: React.MouseEventHandler = domEvent => {
+ triggerChildrenActive(false);
- onMouseLeave?.({
- key: eventKey,
- domEvent,
- });
- };
+ onMouseLeave?.({
+ key: eventKey,
+ domEvent,
+ });
+ };
const mergedActive = React.useMemo(() => {
if (active) {
@@ -217,7 +218,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
});
// Trigger open by click when mode is `inline`
- if (mode === 'inline') {
+ if (mode === 'inline' && !isMultiMode.isMultiPopup) {
onOpenChange(eventKey, !originOpen);
}
};
@@ -233,6 +234,9 @@ const InternalSubMenu = (props: SubMenuProps) => {
if (mode !== 'inline') {
onOpenChange(eventKey, newVisible);
}
+ if (isMultiMode.isMultiPopup) {
+ onOpenChange(eventKey, newVisible);
+ }
};
/**
@@ -287,6 +291,10 @@ const InternalSubMenu = (props: SubMenuProps) => {
triggerModeRef.current = connectedPath.length > 1 ? 'vertical' : mode;
}
+ if (isMultiMode.isMultiPopup) {
+ triggerModeRef.current = 'vertical';
+ }
+
if (!overflowDisabled) {
const triggerMode = triggerModeRef.current;
@@ -296,7 +304,11 @@ const InternalSubMenu = (props: SubMenuProps) => {
{
// Special handle of horizontal mode
mode={triggerMode === 'horizontal' ? 'vertical' : triggerMode}
>
-
- {children}
-
+ {!isMultiMode.isMulti || isMultiMode.isPopup ? (
+
+ {children}
+
+ ) : null}
}
disabled={mergedDisabled}
@@ -339,6 +353,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
[`${subMenuPrefixCls}-active`]: mergedActive,
[`${subMenuPrefixCls}-selected`]: childrenSelected,
[`${subMenuPrefixCls}-disabled`]: mergedDisabled,
+ [`${subMenuPrefixCls}-multi`]: isMultiMode.isMultiPopup,
},
)}
onMouseEnter={onInternalMouseEnter}
@@ -347,7 +362,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
{titleNode}
{/* Inline mode */}
- {!overflowDisabled && (
+ {!overflowDisabled && (!isMultiMode.isMulti || !isMultiMode.isPopup) && (
{children}
diff --git a/src/context/MenuContext.tsx b/src/context/MenuContext.tsx
index f67090ea..082616a1 100644
--- a/src/context/MenuContext.tsx
+++ b/src/context/MenuContext.tsx
@@ -17,6 +17,7 @@ export interface MenuContextProps {
// Mode
mode: MenuMode;
+ inlineMaxDeep?: number;
// Disabled
disabled?: boolean;
diff --git a/src/utils/multiModeUtil.ts b/src/utils/multiModeUtil.ts
new file mode 100644
index 00000000..1db02b9a
--- /dev/null
+++ b/src/utils/multiModeUtil.ts
@@ -0,0 +1,31 @@
+import type { MenuMode } from '../interface';
+
+export function genMultiMode(
+ keys: string[],
+ mode?: MenuMode,
+ inlineMaxDeep?: number,
+): {
+ isMulti: boolean;
+ isPopup: boolean;
+ isMultiPopup: boolean;
+} {
+ const multi = {
+ isMulti: false,
+ isPopup: false,
+ isMultiPopup: false,
+ };
+
+ if (mode === 'inline' && typeof inlineMaxDeep === 'number') {
+ multi.isMulti = true;
+ }
+
+ if (keys?.length >= inlineMaxDeep) {
+ multi.isPopup = true;
+ }
+
+ if (multi.isMulti && multi.isPopup) {
+ multi.isMultiPopup = true;
+ }
+
+ return multi;
+}
diff --git a/tests/Menu.spec.js b/tests/Menu.spec.js
index 4f2b93c9..0ea3909e 100644
--- a/tests/Menu.spec.js
+++ b/tests/Menu.spec.js
@@ -623,5 +623,113 @@ describe('Menu', () => {
jest.useRealTimers();
});
});
+
+ function createInlineMenu(level, onOpenChange) {
+ return (
+
+ );
+ }
+
+ describe('Click or hover inline Menu with inlineMaxDeep', () => {
+ [
+ {
+ level: null,
+ openKeys: [],
+ },
+ {
+ level: -1,
+ openKeys: [],
+ },
+ {
+ level: 0,
+ openKeys: [],
+ },
+ {
+ level: 1,
+ openKeys: [],
+ },
+ {
+ level: 2,
+ openKeys: ['4'],
+ },
+ {
+ level: 3,
+ openKeys: [],
+ },
+ ].forEach(item => {
+ const { level, openKeys } = item;
+ it(`Click on the ${level} level menu is open, the other menus is closed`, async () => {
+ jest.useFakeTimers();
+
+ const onOpenChange = jest.fn();
+
+ const wrapper = mount(createInlineMenu(level, onOpenChange));
+
+ await act(async () => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+
+ const menuItems = wrapper.find('.rc-menu-item');
+ menuItems.at(0).simulate('click');
+
+ if (typeof level === 'number' && level < 3) {
+ expect(onOpenChange).toHaveBeenCalledWith(openKeys);
+ } else {
+ expect(onOpenChange).not.toHaveBeenCalled();
+ }
+
+ jest.useRealTimers();
+ });
+
+ it(`Hover on the ${level} level menu is open, the other menus is closed`, async () => {
+ jest.useFakeTimers();
+
+ const onOpenChange = jest.fn();
+
+ const wrapper = mount(createInlineMenu(level, onOpenChange));
+
+ await act(async () => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+
+ // Enter
+ wrapper.find('.rc-menu-submenu-title').at(1).simulate('mouseEnter');
+ await act(async () => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+ if (typeof level === 'number' && level < 3) {
+ expect(
+ wrapper.find('PopupTrigger').at(1).prop('visible'),
+ ).toBeTruthy();
+ }
+
+ // Leave
+ wrapper.find('.rc-menu-submenu-title').at(1).simulate('mouseLeave');
+ await act(async () => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('PopupTrigger').at(1).prop('visible')).toBeFalsy();
+
+ jest.useRealTimers();
+ });
+ });
+ });
});
/* eslint-enable */