diff --git a/README.md b/README.md index 13de7032..904af748 100644 --- a/README.md +++ b/README.md @@ -75,18 +75,6 @@ ReactDOM.render( vertical one of ["horizontal","inline","vertical-left","vertical-right"] - - activeKey - String - - initial and current active menu item's key. - - - defaultActiveFirst - Boolean - false - whether active first menu item when show if activeKey is not set or invalid - multiple Boolean diff --git a/assets/index.less b/assets/index.less index 1d2f9338..032a208f 100644 --- a/assets/index.less +++ b/assets/index.less @@ -53,11 +53,6 @@ border-bottom: 1px solid #dedede; } - &-item-active, - &-submenu-active > &-submenu-title { - background-color: #eaf8fe; - } - &-item-selected { background-color: #eaf8fe; // fix chrome render bug @@ -80,6 +75,38 @@ margin-top: 0; } + .active-effect(@background-color) { + &:focus, + &:hover { + background-color: @background-color; + } + } + + &-submenu { + &-title { + .active-effect(#eaf8fe); + } + + &-open { + background-color: #eaf8fe; + } + + &-disabled { + &-open { + background-color: transparent; + } + .active-effect(transparent); + } + } + + &-item { + .active-effect(#eaf8fe); + + &-disabled { + .active-effect(transparent); + } + } + &-item, &-submenu-title { margin: 0; diff --git a/docs/demo/performance.md b/docs/demo/performance.md new file mode 100644 index 00000000..2d64da5d --- /dev/null +++ b/docs/demo/performance.md @@ -0,0 +1,3 @@ +## performance + + 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: +
+ +
+ + {new Array(count).fill(0).map((m, i) => { + const title = 'item - ' + i + + return ( + + {new Array(count).fill(0).map((n, index) => { + const key = i + ' - ' + index + const subTitle = title + ' - ' + index + + return ( + {subTitle} + ) + })} + + ) + })} + +
+
+ ) +} + +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( @@ -73,27 +62,35 @@ describe('Menu.Keyboard', () => { const wrapper = mount(, { attachTo: holder }); + wrapper.setState({ items: [0, 1] }); + await wrapper.flush(); + + const menuItems = document.querySelectorAll('.rc-menu-item'); + // First item keyDown(wrapper, KeyCode.DOWN); - expect(wrapper.isActive(0)).toBeTruthy(); + expect(menuItems[0] === document.activeElement).toBeTruthy(); + keyDown(wrapper, KeyCode.ENTER); + expect( + menuItems[0].classList.contains('rc-menu-item-selected'), + ).toBeTruthy(); - // Next item + // // Next item keyDown(wrapper, KeyCode.DOWN); - expect(wrapper.isActive(1)).toBeTruthy(); + expect(menuItems[1] === document.activeElement).toBeTruthy(); - // Very first item - keyDown(wrapper, KeyCode.HOME); - expect(wrapper.isActive(0)).toBeTruthy(); - - // Very last item - keyDown(wrapper, KeyCode.END); - expect(wrapper.isActive(2)).toBeTruthy(); + keyDown(wrapper, KeyCode.ENTER); + expect( + menuItems[0].classList.contains('rc-menu-item-selected'), + ).toBeFalsy(); + expect( + menuItems[1].classList.contains('rc-menu-item-selected'), + ).toBeTruthy(); }); it('Skip disabled item', () => { const wrapper = mount( - - + 1 2 @@ -102,27 +99,26 @@ describe('Menu.Keyboard', () => { { attachTo: holder }, ); + const menuItems = document.querySelectorAll('.rc-menu-item'); + // Next item keyDown(wrapper, KeyCode.DOWN); - keyDown(wrapper, KeyCode.DOWN); - expect(wrapper.isActive(3)).toBeTruthy(); - - // Back to first item - keyDown(wrapper, KeyCode.UP); - expect(wrapper.isActive(1)).toBeTruthy(); - - // To the last available item - keyDown(wrapper, KeyCode.END); - expect(wrapper.isActive(3)).toBeTruthy(); + keyDown(wrapper, KeyCode.ENTER); + expect( + menuItems[0].classList.contains('rc-menu-item-selected'), + ).toBeTruthy(); - // To the first available item - keyDown(wrapper, KeyCode.HOME); - expect(wrapper.isActive(1)).toBeTruthy(); + // Go to next item + keyDown(wrapper, KeyCode.DOWN); + keyDown(wrapper, KeyCode.ENTER); + expect( + menuItems[2].classList.contains('rc-menu-item-selected'), + ).toBeTruthy(); }); - it('Enter to open menu and active first item', () => { + it('Enter to open submenu and focus first item', () => { const wrapper = mount( - + 1 @@ -130,12 +126,15 @@ describe('Menu.Keyboard', () => { { attachTo: holder }, ); - // Active first sub menu - keyDown(wrapper, KeyCode.DOWN); - // Open it + keyDown(wrapper, KeyCode.DOWN); keyDown(wrapper, KeyCode.ENTER); expect(wrapper.find('PopupTrigger').prop('visible')).toBeTruthy(); + + const popupMenuItems = document + .querySelector('.rc-menu-sub') + .querySelectorAll('.rc-menu-item'); + expect(popupMenuItems[0] === document.activeElement).toBeTruthy(); }); describe('go to children & back of parent', () => { @@ -156,35 +155,35 @@ describe('Menu.Keyboard', () => { { attachTo: holder }, ); - // Active first + let sumMenuTitles; + + // Focus first keyDown(wrapper, KeyCode.DOWN); + sumMenuTitles = document.querySelectorAll('.rc-menu-submenu-title'); + expect(sumMenuTitles[0] === document.activeElement).toBeTruthy(); - // Open and active sub + // Open and focus sub keyDown(wrapper, subKey); expect( wrapper.find('PopupTrigger').first().prop('visible'), ).toBeTruthy(); - expect( - wrapper - .find('.rc-menu-submenu-active .rc-menu-submenu-title') - .last() - .text(), - ).toEqual('Light'); + sumMenuTitles = document.querySelectorAll('.rc-menu-submenu-title'); + expect(sumMenuTitles[1] === document.activeElement).toBeTruthy(); - // Open and active sub + // Open and focus sub keyDown(wrapper, subKey); expect( wrapper.find('PopupTrigger').last().prop('visible'), ).toBeTruthy(); - expect(wrapper.find('.rc-menu-item-active').last().text()).toEqual( - 'Little', - ); + + const menuItems = document.querySelectorAll('.rc-menu-item'); + expect(menuItems[0] === document.activeElement).toBeTruthy(); // Back to parent keyDown(wrapper, parentKey); expect(wrapper.find('PopupTrigger').last().prop('visible')).toBeFalsy(); - expect(wrapper.find('.rc-menu-item-active')).toHaveLength(0); + expect(sumMenuTitles[1] === document.activeElement).toBeTruthy(); // Back to parent keyDown(wrapper, parentKey); @@ -193,7 +192,7 @@ describe('Menu.Keyboard', () => { wrapper.find('PopupTrigger').first().prop('visible'), ).toBeFalsy(); - expect(wrapper.find('li.rc-menu-submenu-active')).toHaveLength(1); + expect(sumMenuTitles[0] === document.activeElement).toBeTruthy(); wrapper.unmount(); }); @@ -214,16 +213,22 @@ describe('Menu.Keyboard', () => { { attachTo: holder }, ); + const menuRoot = document.querySelector('.rc-menu-root'); + // Nothing happen when no control key keyDown(wrapper, KeyCode.P); - expect(wrapper.exists('.rc-menu-item-active')).toBeFalsy(); + expect(document.body === document.activeElement).toBeTruthy(); - // Active first + // Focus first keyDown(wrapper, KeyCode.DOWN); - expect(wrapper.isActive(0)).toBeTruthy(); + expect(menuRoot.childNodes[0] === document.activeElement).toBeTruthy(); - // Active next + // Focus next keyDown(wrapper, KeyCode.DOWN); + expect( + menuRoot.querySelector('.rc-menu-submenu-title') === + document.activeElement, + ); // Right will not open keyDown(wrapper, KeyCode.RIGHT); @@ -233,16 +238,14 @@ describe('Menu.Keyboard', () => { keyDown(wrapper, KeyCode.ENTER); expect(wrapper.find('InlineSubMenuList').prop('open')).toBeTruthy(); expect( - wrapper - .find('.rc-menu-submenu') - .last() - .hasClass('rc-menu-submenu-active'), + wrapper.find('.rc-menu-submenu').last().hasClass('rc-menu-submenu-open'), ).toBeTruthy(); - expect(wrapper.isActive(1)).toBeFalsy(); - // Active sub item + // Focus sub item keyDown(wrapper, KeyCode.DOWN); - expect(wrapper.isActive(1)).toBeTruthy(); + + const items = document.querySelectorAll('.rc-menu-item'); + expect(items[items.length - 1] === document.activeElement).toBeTruthy(); }); it('Focus last one', () => { @@ -254,8 +257,12 @@ describe('Menu.Keyboard', () => { { attachTo: holder }, ); + const menuItems = document.querySelectorAll('.rc-menu-item'); + keyDown(wrapper, KeyCode.UP); - expect(wrapper.isActive(1)).toBeTruthy(); + keyDown(wrapper, KeyCode.ENTER); + + expect(menuItems[1] === document.activeElement).toBeTruthy(); }); it('Focus to link direct', () => { @@ -269,11 +276,12 @@ describe('Menu.Keyboard', () => { ); const focusSpy = jest.spyOn( - (wrapper.find('a').instance() as any) as HTMLAnchorElement, + wrapper.find('a').instance() as any as HTMLAnchorElement, 'focus', ); keyDown(wrapper, KeyCode.DOWN); + keyDown(wrapper, KeyCode.ENTER); expect(focusSpy).toHaveBeenCalled(); }); @@ -288,7 +296,32 @@ describe('Menu.Keyboard', () => { keyDown(wrapper, KeyCode.DOWN); keyDown(wrapper, KeyCode.LEFT); keyDown(wrapper, KeyCode.RIGHT); - expect(wrapper.isActive(0)).toBeTruthy(); + + const menuItems = document.querySelectorAll('.rc-menu-item'); + + expect(menuItems[0] === document.activeElement).toBeTruthy(); + }); + + it('Test KeyCode.HOME and KeyCode.END', () => { + const wrapper = mount( + + foo + bar + baz + , + { attachTo: holder }, + ); + + const menuItems = document.querySelectorAll('.rc-menu-item'); + + keyDown(wrapper, KeyCode.END); + expect(menuItems[2] === document.activeElement).toBeTruthy(); + + keyDown(wrapper, KeyCode.HOME); + expect(menuItems[0] === document.activeElement).toBeTruthy(); + + keyDown(wrapper, KeyCode.UP); + expect(menuItems[2] === document.activeElement).toBeTruthy(); }); }); /* eslint-enable */ diff --git a/tests/Menu.spec.js b/tests/Menu.spec.js index 430fa1b7..69930042 100644 --- a/tests/Menu.spec.js +++ b/tests/Menu.spec.js @@ -133,37 +133,10 @@ describe('Menu', () => { }); }); - it('set activeKey', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect(wrapper.isActive(0)).toBeTruthy(); - expect(wrapper.isActive(1)).toBeFalsy(); - - wrapper.setProps({ activeKey: '2' }); - expect(wrapper.isActive(0)).toBeFalsy(); - expect(wrapper.isActive(1)).toBeTruthy(); - }); - - it('active first item', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper.find('.rc-menu-item').first().hasClass('rc-menu-item-active'), - ).toBeTruthy(); - }); - it('should render none menu item children', () => { expect(() => { mount( - + 1 2 string @@ -407,43 +380,6 @@ describe('Menu', () => { expect(handleDeselect.mock.calls[0][0].key).toBe('1'); }); - it('active by mouse enter', () => { - const wrapper = mount( - - item - disabled - item2 - , - ); - wrapper.find('li').last().simulate('mouseEnter'); - expect(wrapper.isActive(2)).toBeTruthy(); - }); - - it('active by key down', () => { - const wrapper = mount( - - 1 - 2 - , - ); - - // KeyDown will not change activeKey since control - wrapper.find('Overflow').simulate('keyDown', { which: KeyCode.DOWN }); - expect(wrapper.isActive(0)).toBeTruthy(); - - wrapper.setProps({ activeKey: '2' }); - expect(wrapper.isActive(1)).toBeTruthy(); - }); - - it('defaultActiveFirst', () => { - const wrapper = mount( - - foo - , - ); - expect(wrapper.isActive(0)).toBeTruthy(); - }); - it('should accept builtinPlacements', () => { const builtinPlacements = { leftTop: { @@ -551,18 +487,6 @@ describe('Menu', () => { expect(onMouseEnter).toHaveBeenCalled(); }); - it('Nest children active should bump to top', async () => { - const wrapper = mount( - - - Light - - , - ); - - expect(wrapper.exists('.rc-menu-submenu-active')).toBeTruthy(); - }); - it('not warning on destroy', async () => { const errorSpy = jest.spyOn(console, 'error'); diff --git a/tests/Responsive.spec.tsx b/tests/Responsive.spec.tsx index e06eadec..2e9afca1 100644 --- a/tests/Responsive.spec.tsx +++ b/tests/Responsive.spec.tsx @@ -2,19 +2,36 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import KeyCode from 'rc-util/lib/KeyCode'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import { render } from 'enzyme'; import ResizeObserver from 'rc-resize-observer'; -import { mount } from './util'; +import { mount, keyDown } from './util'; import Menu, { MenuItem, SubMenu } from '../src'; import { OVERFLOW_KEY } from '../src/hooks/useKeyRecords'; describe('Menu.Responsive', () => { + let holder: HTMLDivElement; + + beforeAll(() => { + // Mock to force make menu item visible + spyElementPrototypes(HTMLElement, { + offsetParent: { + get() { + return this.parentElement; + }, + }, + }); + }); + beforeEach(() => { + holder = document.createElement('div'); + document.body.appendChild(holder); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); + holder.parentElement.removeChild(holder); }); it('ssr render full', () => { @@ -30,16 +47,17 @@ describe('Menu.Responsive', () => { }); it('show rest', () => { - const onOpenChange = jest.fn(); + const onOpenChange = jest.fn(openedKeys => openedKeys); const wrapper = mount( - + Light Bamboo Little + Foo , - { attachTo: document.body }, + { attachTo: holder }, ); act(() => { @@ -64,7 +82,7 @@ describe('Menu.Responsive', () => { item .find(ResizeObserver) .props() - .onResize({ offsetWidth: 20 } as any, null); + .onResize({ offsetWidth: 20, width: 20 } as any, {} as any); jest.runAllTimers(); wrapper.update(); }); @@ -75,21 +93,18 @@ describe('Menu.Responsive', () => { wrapper.find('.rc-menu-overflow-item-rest').last().prop('style').opacity, ).not.toEqual(0); - // Should set active on rest - expect( - wrapper - .find('.rc-menu-overflow-item-rest') - .last() - .hasClass('rc-menu-submenu-active'), - ).toBeTruthy(); - - // Key down can open expect(onOpenChange).not.toHaveBeenCalled(); - wrapper.setProps({ activeKey: OVERFLOW_KEY }); - wrapper - .find('ul.rc-menu-root') - .simulate('keyDown', { which: KeyCode.DOWN }); + + // open submenu + keyDown(wrapper, KeyCode.DOWN); + keyDown(wrapper, KeyCode.RIGHT); + keyDown(wrapper, KeyCode.ENTER); expect(onOpenChange).toHaveBeenCalled(); + + // go to next SubMenu and open + keyDown(wrapper, KeyCode.DOWN); + keyDown(wrapper, KeyCode.ENTER); + expect(onOpenChange.mock.results[1].value).toEqual([OVERFLOW_KEY, 'home']); }); }); /* eslint-enable */ diff --git a/tests/SubMenu.spec.js b/tests/SubMenu.spec.js index 41b9bf17..ee65e9ff 100644 --- a/tests/SubMenu.spec.js +++ b/tests/SubMenu.spec.js @@ -255,34 +255,6 @@ describe('SubMenu', () => { }); describe('mouse events', () => { - it('mouse enter event on a submenu should not activate first item', () => { - const wrapper = mount(createMenu({ openKeys: ['s1'] })); - const title = wrapper.find('div.rc-menu-submenu-title').first(); - title.simulate('mouseEnter'); - - act(() => { - jest.runAllTimers(); - wrapper.update(); - }); - - expect(wrapper.find('PopupTrigger').first().prop('visible')).toBeTruthy(); - expect(wrapper.isActive(0)).toBeFalsy(); - }); - - it('click to open a submenu should not activate first item', () => { - const wrapper = mount(createMenu({ triggerSubMenuAction: 'click' })); - const subMenuTitle = wrapper.find('div.rc-menu-submenu-title').first(); - subMenuTitle.simulate('click'); - - act(() => { - jest.runAllTimers(); - wrapper.update(); - }); - - expect(wrapper.find('PopupTrigger').first().prop('visible')).toBeTruthy(); - expect(wrapper.isActive(0)).toBeFalsy(); - }); - it('mouse enter/mouse leave on a subMenu item should trigger hooks', () => { const onMouseEnter = jest.fn(); const onMouseLeave = jest.fn(); diff --git a/tests/__snapshots__/MenuItem.spec.js.snap b/tests/__snapshots__/MenuItem.spec.js.snap index d4bc1606..90dff060 100644 --- a/tests/__snapshots__/MenuItem.spec.js.snap +++ b/tests/__snapshots__/MenuItem.spec.js.snap @@ -48,13 +48,14 @@ exports[`MenuItem overwrite default role should set role to option 1`] = ` exports[`MenuItem rest props onClick event should only trigger 1 time along the component hierarchy 1`] = ` Array [