Skip to content

feat: add api inlineMaxLevel #421

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -219,6 +219,12 @@ ReactDOM.render(
<th>24</th>
<td>Padding level multiplier. Right or left padding depends on param "direction".</td>
</tr>
<tr>
<td>inlineMaxDeep</td>
<td>Number</td>
<th></th>
<td>inline menu, specify at most a certain deep of submenu, deeper submenu will right popover</td>
</tr>
</tbody>
</table>

15 changes: 15 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
@@ -227,6 +227,21 @@
}
}

&-inline {
.@{menuPrefixCls}-submenu-multi {
.@{menuPrefixCls}-submenu-title {
.@{menuPrefixCls}-submenu-arrow {
transform: rotate(0);
}
}
}
.@{menuPrefixCls}-submenu-multi.@{menuPrefixCls}-submenu-open {
.@{menuPrefixCls}-submenu-title {
background-color: #eaf8fe;
}
}
}

&-vertical&-sub,
&-vertical-left&-sub,
&-vertical-right&-sub {
5 changes: 5 additions & 0 deletions docs/demo/antd-switch-multi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## antd-switch-multi

inline Menu, up to two level menus, more submenus right popover

<code src="../examples/antd-switch-multi.tsx">
44 changes: 44 additions & 0 deletions docs/examples/antd-switch-multi.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ margin: 20, width: 200 }}>
<label>
<input
type="checkbox"
checked={inline}
onChange={() => setInline(!inline)}
/>{' '}
Inline
</label>
<CommonMenu
mode="inline"
inlineMaxDeep={2}
openKeys={openKeys}
onOpenChange={keys => {
console.error('Open Keys Changed:', keys);
setOpenKey(keys);
}}
inlineCollapsed={!inline}
{...restProps}
/>
</div>
);
};

export default Demo;
/* eslint-enable */
32 changes: 25 additions & 7 deletions src/Menu.tsx
Original file line number Diff line number Diff line change
@@ -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<MenuProps> = props => {

// Mode
mode = 'vertical',
inlineMaxDeep,
inlineCollapsed,

// Disabled
@@ -227,14 +230,17 @@ const Menu: React.FC<MenuProps> = 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<MenuProps> = 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<MenuProps> = 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<MenuProps> = 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<MenuProps> = props => {
<MenuContextProvider
prefixCls={prefixCls}
mode={mergedMode}
inlineMaxDeep={inlineMaxDeep}
openKeys={mergedOpenKeys}
rtl={isRtl}
// Disabled
59 changes: 37 additions & 22 deletions src/SubMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import PopupTrigger from './PopupTrigger';
import Icon from '../Icon';
import useActive from '../hooks/useActive';
import { warnItemProp } from '../utils/warnUtil';
import { genMultiMode } from '../utils/multiModeUtil';
import useDirectionStyle from '../hooks/useDirectionStyle';
import InlineSubMenuList from './InlineSubMenuList';
import {
@@ -104,6 +105,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
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<HTMLLIElement> =
domEvent => {
triggerChildrenActive(true);
const onInternalMouseEnter: React.MouseEventHandler<HTMLLIElement> = domEvent => {
triggerChildrenActive(true);

onMouseEnter?.({
key: eventKey,
domEvent,
});
};
onMouseEnter?.({
key: eventKey,
domEvent,
});
};

const onInternalMouseLeave: React.MouseEventHandler<HTMLLIElement> =
domEvent => {
triggerChildrenActive(false);
const onInternalMouseLeave: React.MouseEventHandler<HTMLLIElement> = 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,17 +304,23 @@ const InternalSubMenu = (props: SubMenuProps) => {
<PopupTrigger
mode={triggerMode}
prefixCls={subMenuPrefixCls}
visible={!internalPopupClose && open && mode !== 'inline'}
visible={
!internalPopupClose &&
open &&
(mode !== 'inline' || isMultiMode.isMultiPopup)
}
popupClassName={popupClassName}
popupOffset={popupOffset}
popup={
<MenuContextProvider
// Special handle of horizontal mode
mode={triggerMode === 'horizontal' ? 'vertical' : triggerMode}
>
<SubMenuList id={popupId} ref={popupRef}>
{children}
</SubMenuList>
{!isMultiMode.isMulti || isMultiMode.isPopup ? (
<SubMenuList id={popupId} ref={popupRef}>
{children}
</SubMenuList>
) : null}
</MenuContextProvider>
}
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) && (
<InlineSubMenuList id={popupId} open={open} keyPath={connectedPath}>
{children}
</InlineSubMenuList>
1 change: 1 addition & 0 deletions src/context/MenuContext.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export interface MenuContextProps {

// Mode
mode: MenuMode;
inlineMaxDeep?: number;

// Disabled
disabled?: boolean;
31 changes: 31 additions & 0 deletions src/utils/multiModeUtil.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading