Skip to content

Commit c7f9880

Browse files
authored
fix: fix focus when active element is removed (#791)
1 parent c1b586d commit c7f9880

File tree

2 files changed

+76
-5
lines changed

2 files changed

+76
-5
lines changed

src/Menu.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,14 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
399399
const { elements, key2element, element2key } = refreshElements(keys, uuid);
400400
const focusableElements = getFocusableElements(containerRef.current, elements);
401401

402-
const shouldFocusKey =
403-
mergedActiveKey ??
404-
(focusableElements[0]
402+
let shouldFocusKey: string;
403+
if (mergedActiveKey && keys.includes(mergedActiveKey)) {
404+
shouldFocusKey = mergedActiveKey;
405+
} else {
406+
shouldFocusKey = focusableElements[0]
405407
? element2key.get(focusableElements[0])
406-
: childList.find(node => !node.props.disabled)?.key);
407-
408+
: childList.find(node => !node.props.disabled)?.key;
409+
}
408410
const elementToFocus = key2element.get(shouldFocusKey);
409411

410412
if (shouldFocusKey && elementToFocus) {

tests/Menu.spec.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { act } from 'react-dom/test-utils';
88
import type { MenuRef } from '../src';
99
import Menu, { Divider, MenuItem, MenuItemGroup, SubMenu } from '../src';
1010
import { isActive, last } from './util';
11+
import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
1112

1213
jest.mock('@rc-component/trigger', () => {
1314
const react = require('react');
@@ -790,5 +791,73 @@ describe('Menu', () => {
790791
expect(catItem).toBe(container.querySelectorAll('li')[1]);
791792
expect(nonExistentItem).toBe(null);
792793
});
794+
795+
it('should correctly handle invalid mergedActiveKey and fallback to focusable elements', async () => {
796+
// Mock to force make menu item visible
797+
const domSpy = spyElementPrototypes(HTMLElement, {
798+
offsetParent: {
799+
get() {
800+
return this.parentElement;
801+
},
802+
},
803+
});
804+
805+
const menuRef = React.createRef<MenuRef>();
806+
807+
const TestApp = () => {
808+
const [items, setItems] = React.useState([
809+
{ key: 'item1', label: 'First Item' },
810+
{ key: 'item2', label: 'Second Item' },
811+
{ key: 'item3', label: 'Third Item' },
812+
]);
813+
814+
const removeSecondItem = () => {
815+
setItems(prevItems => prevItems.filter(item => item.key !== 'item2'));
816+
};
817+
818+
return (
819+
<div>
820+
<Menu data-testid="menu" activeKey="item2" tabIndex={0} ref={menuRef}>
821+
{items.map(item => (
822+
<MenuItem key={item.key} data-testid={item.key}>
823+
{item.label}
824+
</MenuItem>
825+
))}
826+
</Menu>
827+
<button data-testid="remove-button" onClick={removeSecondItem} tabIndex={0}>
828+
Remove Second Item
829+
</button>
830+
</div>
831+
);
832+
};
833+
834+
const { getByTestId } = render(<TestApp />);
835+
836+
const removeButton = getByTestId('remove-button');
837+
838+
// Verify initial state - item2 should be active
839+
expect(getByTestId('item2')).toHaveClass('rc-menu-item-active');
840+
841+
// Remove the active item (item2)
842+
await act(async () => {
843+
fireEvent.click(removeButton);
844+
});
845+
846+
// Verify the item is removed
847+
expect(() => getByTestId('item2')).toThrow();
848+
849+
// mock focus on item 1 to make sure it gets focused
850+
const item1 = getByTestId('item1');
851+
const focusSpy = jest.spyOn(item1, 'focus').mockImplementation(() => {});
852+
853+
// when we call focus(), it should properly handle the case where
854+
// mergedActiveKey ("item2") no longer exists
855+
menuRef.current.focus();
856+
857+
expect(focusSpy).toHaveBeenCalled();
858+
// cleanup
859+
focusSpy.mockRestore();
860+
domSpy?.mockRestore?.();
861+
});
793862
});
794863
/* eslint-enable */

0 commit comments

Comments
 (0)