diff --git a/README.md b/README.md index 13de7032..904af748 100644 --- a/README.md +++ b/README.md @@ -75,18 +75,6 @@ ReactDOM.render(
diff --git a/docs/examples/performance.tsx b/docs/examples/performance.tsx
new file mode 100644
index 00000000..ee05d0e5
--- /dev/null
+++ b/docs/examples/performance.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react'
+import Menu from '../../src/Menu'
+import SubMenu from '../../src/SubMenu'
+import MenuItem from '../../src/MenuItem'
+import type { MenuMode } from '../../src/interface'
+
+const count = 50
+
+const Performance = () => {
+ const [mode, setMode] = React.useState('vertical')
+
+ return (
+
+
+ mode:
+
+
+
+
+
+
+ )
+}
+
+export default Performance
diff --git a/src/Menu.tsx b/src/Menu.tsx
index a28b4bf2..57c08624 100644
--- a/src/Menu.tsx
+++ b/src/Menu.tsx
@@ -69,10 +69,6 @@ export interface MenuProps
defaultOpenKeys?: string[];
openKeys?: string[];
- // Active control
- activeKey?: string;
- defaultActiveFirst?: boolean;
-
// Selection
selectable?: boolean;
multiple?: boolean;
@@ -135,7 +131,6 @@ export interface MenuProps
stateProps: {
selected: boolean;
open: boolean;
- active: boolean;
disabled: boolean;
},
) => React.ReactElement;
@@ -172,10 +167,6 @@ const Menu = React.forwardRef((props, ref) => {
defaultOpenKeys,
openKeys,
- // Active
- activeKey,
- defaultActiveFirst,
-
// Selection
selectable = true,
multiple = false,
@@ -328,22 +319,6 @@ const Menu = React.forwardRef((props, ref) => {
);
}, [lastVisibleIndex, allVisible]);
- // ======================== Active ========================
- const [mergedActiveKey, setMergedActiveKey] = useMergedState(
- activeKey || ((defaultActiveFirst && childList[0]?.key) as string),
- {
- value: activeKey,
- },
- );
-
- const onActive = useMemoCallback((key: string) => {
- setMergedActiveKey(key);
- });
-
- const onInactive = useMemoCallback(() => {
- setMergedActiveKey(undefined);
- });
-
// ======================== Select ========================
// >>>>> Select keys
const [mergedSelectKeys, setMergedSelectKeys] = useMergedState(
@@ -441,7 +416,6 @@ const Menu = React.forwardRef((props, ref) => {
const onInternalKeyDown = useAccessibility(
mergedMode,
- mergedActiveKey,
isRtl,
uuid,
@@ -449,7 +423,6 @@ const Menu = React.forwardRef((props, ref) => {
getKeys,
getKeyPath,
- setMergedActiveKey,
triggerAccessibilityOpen,
onKeyDown,
@@ -557,10 +530,6 @@ const Menu = React.forwardRef((props, ref) => {
// Motion
motion={mounted ? motion : null}
defaultMotions={mounted ? defaultMotions : null}
- // Active
- activeKey={mergedActiveKey}
- onActive={onActive}
- onInactive={onInactive}
// Selection
selectedKeys={mergedSelectKeys}
// Level
diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx
index 5e7a724b..95385188 100644
--- a/src/MenuItem.tsx
+++ b/src/MenuItem.tsx
@@ -11,7 +11,7 @@ import type {
RenderIconType,
} from './interface';
import { MenuContext } from './context/MenuContext';
-import useActive from './hooks/useActive';
+import useMouseEvents from './hooks/useMouseEvents';
import { warnItemProp } from './utils/warnUtil';
import Icon from './Icon';
import useDirectionStyle from './hooks/useDirectionStyle';
@@ -113,9 +113,6 @@ const InternalMenuItem = (props: MenuItemProps) => {
// Select
selectedKeys,
-
- // Active
- onActive,
} = React.useContext(MenuContext);
const { _internalRenderMenuItem } = React.useContext(PrivateContext);
@@ -150,7 +147,7 @@ const InternalMenuItem = (props: MenuItemProps) => {
const mergedItemIcon = itemIcon || contextItemIcon;
// ============================ Active ============================
- const { active, ...activeProps } = useActive(
+ const mouseEvents = useMouseEvents(
eventKey,
mergedDisabled,
onMouseEnter,
@@ -187,15 +184,6 @@ const InternalMenuItem = (props: MenuItemProps) => {
}
};
- /**
- * Used for accessibility. Helper will focus element without key board.
- * We should manually trigger an active
- */
- const onInternalFocus: React.FocusEventHandler = e => {
- onActive(eventKey);
- onFocus?.(e);
- };
-
// ============================ Render ============================
const optionRoleProps: React.HTMLAttributes = {};
@@ -211,7 +199,7 @@ const InternalMenuItem = (props: MenuItemProps) => {
tabIndex={disabled ? null : -1}
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
{...restProps}
- {...activeProps}
+ {...mouseEvents}
{...optionRoleProps}
component="li"
aria-disabled={disabled}
@@ -222,7 +210,6 @@ const InternalMenuItem = (props: MenuItemProps) => {
className={classNames(
itemCls,
{
- [`${itemCls}-active`]: active,
[`${itemCls}-selected`]: selected,
[`${itemCls}-disabled`]: mergedDisabled,
},
@@ -230,7 +217,6 @@ const InternalMenuItem = (props: MenuItemProps) => {
)}
onClick={onInternalClick}
onKeyDown={onInternalKeyDown}
- onFocus={onInternalFocus}
>
{children}
{
disabled: contextDisabled,
overflowDisabled,
- // ActiveKey
- activeKey,
-
// SelectKey
selectedKeys,
@@ -124,8 +121,6 @@ const InternalSubMenu = (props: SubMenuProps) => {
// Events
onItemClick,
onOpenChange,
-
- onActive,
} = React.useContext(MenuContext);
const { _internalRenderSubMenuItem } = React.useContext(PrivateContext);
@@ -154,56 +149,35 @@ const InternalSubMenu = (props: SubMenuProps) => {
// =============================== Select ===============================
const childrenSelected = isSubPathKey(selectedKeys, eventKey);
- // =============================== Active ===============================
- const { active, ...activeProps } = useActive(
+ // =========================== MouseEventProps ==========================
+ const mouseEvents = useMouseEvents(
eventKey,
mergedDisabled,
onTitleMouseEnter,
onTitleMouseLeave,
);
- // 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,
- });
+ if (!mergedDisabled) {
+ 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);
+ if (!mergedDisabled) {
+ onMouseLeave?.({
+ key: eventKey,
+ domEvent,
+ });
}
-
- return false;
- }, [mode, active, activeKey, childrenActive, eventKey, isSubPathKey]);
+ };
// ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(connectedPath.length);
@@ -240,14 +214,6 @@ const InternalSubMenu = (props: SubMenuProps) => {
}
};
- /**
- * 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`;
@@ -266,8 +232,7 @@ const InternalSubMenu = (props: SubMenuProps) => {
aria-controls={popupId}
aria-disabled={mergedDisabled}
onClick={onInternalTitleClick}
- onFocus={onInternalFocus}
- {...activeProps}
+ {...mouseEvents}
>
{title}
@@ -335,7 +300,6 @@ const InternalSubMenu = (props: SubMenuProps) => {
className,
{
[`${subMenuPrefixCls}-open`]: open,
- [`${subMenuPrefixCls}-active`]: mergedActive,
[`${subMenuPrefixCls}-selected`]: childrenSelected,
[`${subMenuPrefixCls}-disabled`]: mergedDisabled,
},
@@ -357,7 +321,6 @@ const InternalSubMenu = (props: SubMenuProps) => {
if (_internalRenderSubMenuItem) {
listNode = _internalRenderSubMenuItem(listNode, props, {
selected: childrenSelected,
- active: mergedActive,
open,
disabled: mergedDisabled,
});
diff --git a/src/context/MenuContext.tsx b/src/context/MenuContext.tsx
index f67090ea..53adb6ff 100644
--- a/src/context/MenuContext.tsx
+++ b/src/context/MenuContext.tsx
@@ -23,11 +23,6 @@ export interface MenuContextProps {
// Used for overflow only. Prevent hidden node trigger open
overflowDisabled?: boolean;
- // Active
- activeKey: string;
- onActive: (key: string) => void;
- onInactive: (key: string) => void;
-
// Selection
selectedKeys: string[];
diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts
index b60b27bf..3cf70b4f 100644
--- a/src/hooks/useAccessibility.ts
+++ b/src/hooks/useAccessibility.ts
@@ -114,11 +114,8 @@ function findContainerUL(element: HTMLElement): HTMLUListElement {
/**
* Find focused element within element set provided
*/
-function getFocusElement(
- activeElement: HTMLElement,
- elements: Set,
-): HTMLElement {
- let current = activeElement || document.activeElement;
+function getFocusElement(elements: Set): HTMLElement {
+ let current = document.activeElement;
while (current) {
if (elements.has(current as any)) {
@@ -148,11 +145,6 @@ function getNextFocusElement(
focusMenuElement?: HTMLElement,
offset: number = 1,
) {
- // Key on the menu item will not get validate parent container
- if (!parentQueryContainer) {
- return null;
- }
-
// List current level menu item elements
const sameLevelFocusableMenuElementList = getFocusableElements(
parentQueryContainer,
@@ -183,7 +175,6 @@ function getNextFocusElement(
export default function useAccessibility(
mode: MenuMode,
- activeKey: string,
isRtl: boolean,
id: string,
@@ -191,16 +182,12 @@ export default function useAccessibility(
getKeys: () => string[],
getKeyPath: (key: string, includeOverflow?: boolean) => string[],
- triggerActiveKey: (key: string) => void,
triggerAccessibilityOpen: (key: string, open?: boolean) => void,
originOnKeyDown?: React.KeyboardEventHandler,
): React.KeyboardEventHandler {
const rafRef = React.useRef();
- const activeRef = React.useRef();
- activeRef.current = activeKey;
-
const cleanRaf = () => {
raf.cancel(rafRef.current);
};
@@ -247,8 +234,7 @@ export default function useAccessibility(
refreshElements();
// First we should find current focused MenuItem/SubMenu element
- const activeElement = key2element.get(activeKey);
- const focusMenuElement = getFocusElement(activeElement, elements);
+ const focusMenuElement = getFocusElement(elements);
const focusMenuKey = element2key.get(focusMenuElement);
const offsetObj = getOffset(
@@ -278,9 +264,6 @@ export default function useAccessibility(
focusTargetElement = link;
}
- const targetKey = element2key.get(menuElement);
- triggerActiveKey(targetKey);
-
/**
* Do not `useEffect` here since `tryFocus` may trigger async
* which makes React sync update the `activeKey`
@@ -288,9 +271,7 @@ export default function useAccessibility(
*/
cleanRaf();
rafRef.current = raf(() => {
- if (activeRef.current === targetKey) {
- focusTargetElement.focus();
- }
+ focusTargetElement.focus();
});
}
};
@@ -329,7 +310,6 @@ export default function useAccessibility(
}
// Focus menu item
tryFocus(targetElement);
-
// ======================= InlineTrigger =======================
} else if (offsetObj.inlineTrigger) {
// Inline trigger no need switch to sub menu item
@@ -343,17 +323,32 @@ export default function useAccessibility(
// Async should resync elements
refreshElements();
- const controlId = focusMenuElement.getAttribute('aria-controls');
- const subQueryContainer = document.getElementById(controlId);
+ let targetElement = focusMenuElement;
- // Get sub focusable menu item
- const targetElement = getNextFocusElement(
- subQueryContainer,
- elements,
- );
+ const controlId = targetElement.getAttribute('aria-controls');
+
+ if (controlId) {
+ const subQueryContainer = document.getElementById(controlId);
+
+ // Get sub focusable menu item
+ targetElement = getNextFocusElement(subQueryContainer, elements);
+
+ // Focus menu item
+ tryFocus(targetElement);
+ } else {
+ // select Item
+ targetElement.click();
- // Focus menu item
- tryFocus(targetElement);
+ // back to focus ancestor element
+ if (mode !== 'inline') {
+ const targetElementKey = element2key.get(targetElement);
+ const keyPath = getKeyPath(targetElementKey, true);
+ const ancestorKey = keyPath[0];
+
+ const ancestorMenuElement = key2element.get(ancestorKey);
+ tryFocus(ancestorMenuElement);
+ }
+ }
}, 5);
} else if (offsetObj.offset < 0) {
const keyPath = getKeyPath(focusMenuKey, true);
diff --git a/src/hooks/useActive.ts b/src/hooks/useMouseEvents.ts
similarity index 63%
rename from src/hooks/useActive.ts
rename to src/hooks/useMouseEvents.ts
index dd0d6005..12282348 100644
--- a/src/hooks/useActive.ts
+++ b/src/hooks/useMouseEvents.ts
@@ -1,29 +1,17 @@
-import * as React from 'react';
-import { MenuContext } from '../context/MenuContext';
import type { MenuHoverEventHandler } from '../interface';
interface ActiveObj {
- active: boolean;
onMouseEnter?: React.MouseEventHandler;
onMouseLeave?: React.MouseEventHandler;
}
-export default function useActive(
+export default function useMouseEvents(
eventKey: string,
disabled: boolean,
onMouseEnter?: MenuHoverEventHandler,
onMouseLeave?: MenuHoverEventHandler,
): ActiveObj {
- const {
- // Active
- activeKey,
- onActive,
- onInactive,
- } = React.useContext(MenuContext);
-
- const ret: ActiveObj = {
- active: activeKey === eventKey,
- };
+ const ret: ActiveObj = {};
// Skip when disabled
if (!disabled) {
@@ -32,14 +20,12 @@ export default function useActive(
key: eventKey,
domEvent,
});
- onActive(eventKey);
};
ret.onMouseLeave = domEvent => {
onMouseLeave?.({
key: eventKey,
domEvent,
});
- onInactive(eventKey);
};
}
diff --git a/src/hooks/useUUID.ts b/src/hooks/useUUID.ts
index cce514ee..b7c707ce 100644
--- a/src/hooks/useUUID.ts
+++ b/src/hooks/useUUID.ts
@@ -12,10 +12,8 @@ export default function useUUID(id?: string) {
React.useEffect(() => {
internalId += 1;
- const newId =
- process.env.NODE_ENV === 'test'
- ? 'test'
- : `${uniquePrefix}-${internalId}`;
+ const testEnv = process.env.NODE_ENV === 'test';
+ const newId = testEnv ? 'test' : `${uniquePrefix}-${internalId}`;
setUUID(`rc-menu-uuid-${newId}`);
}, []);
diff --git a/tests/Keyboard.spec.tsx b/tests/Keyboard.spec.tsx
index 07978ff9..19af93aa 100644
--- a/tests/Keyboard.spec.tsx
+++ b/tests/Keyboard.spec.tsx
@@ -1,10 +1,8 @@
/* eslint-disable no-undef, react/no-multi-comp, react/jsx-curly-brace-presence, max-classes-per-file */
import React from 'react';
-import { act } from 'react-dom/test-utils';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import { render } from 'enzyme';
-import { mount } from './util';
-import type { ReactWrapper } from './util';
+import { mount, keyDown } from './util';
import KeyCode from 'rc-util/lib/KeyCode';
import Menu, { MenuItem, SubMenu } from '../src';
@@ -33,15 +31,6 @@ describe('Menu.Keyboard', () => {
holder.parentElement.removeChild(holder);
});
- function keyDown(wrapper: ReactWrapper, keyCode: number) {
- wrapper.find('ul.rc-menu-root').simulate('keyDown', { which: keyCode });
-
- act(() => {
- jest.runAllTimers();
- wrapper.update();
- });
- }
-
it('no data-menu-id by init', () => {
const wrapper = render(