Skip to content

Commit 7274ea2

Browse files
author
liuqiang
committed
fix: No focus issue with scrolling
1 parent 9981c23 commit 7274ea2

File tree

4 files changed

+74
-20
lines changed

4 files changed

+74
-20
lines changed

src/DropdownMenu.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function DropdownMenu(props: DropdownMenuProps) {
2222
onFocus,
2323
onBlur,
2424
onScroll,
25+
textareaRef,
2526
} = React.useContext(MentionsContext);
2627

2728
const { prefixCls, options, opened } = props;
@@ -34,6 +35,14 @@ function DropdownMenu(props: DropdownMenuProps) {
3435
return;
3536
}
3637

38+
// 只有当焦点恰好位于当前文本区域时,才进行滚动操作。
39+
if (
40+
textareaRef?.current &&
41+
document.activeElement !== textareaRef.current.nativeElement
42+
) {
43+
return;
44+
}
45+
3746
const activeItem = menuRef.current?.findItem?.({ key: activeOption.key });
3847

3948
if (activeItem) {
@@ -42,7 +51,7 @@ function DropdownMenu(props: DropdownMenuProps) {
4251
inline: 'nearest',
4352
});
4453
}
45-
}, [activeIndex, activeOption.key, opened]);
54+
}, [activeIndex, activeOption.key, opened, textareaRef]);
4655

4756
return (
4857
<Menu

src/Mentions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ const InternalMentions = forwardRef<MentionsRef, InternalMentionsProps>(
529529
onFocus: onDropdownFocus,
530530
onBlur: onDropdownBlur,
531531
onScroll: onInternalPopupScroll,
532+
textareaRef,
532533
}}
533534
>
534535
<KeywordTrigger

src/MentionsContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* tslint:disable: no-object-literal-type-assertion */
22
import * as React from 'react';
33
import type { OptionProps } from './Option';
4+
import { TextAreaRef } from '@rc-component/textarea';
45

56
export interface MentionsContextProps {
67
notFoundContent: React.ReactNode;
@@ -10,6 +11,7 @@ export interface MentionsContextProps {
1011
onFocus: React.FocusEventHandler<HTMLElement>;
1112
onBlur: React.FocusEventHandler<HTMLElement>;
1213
onScroll: React.UIEventHandler<HTMLElement>;
14+
textareaRef: React.MutableRefObject<TextAreaRef>;
1315
}
1416

1517
// We will never use default, here only to fix TypeScript warning

tests/DropdownMenu.spec.tsx

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,95 @@
11
import React from 'react';
2-
import { render, act } from '@testing-library/react';
3-
import Mentions, { UnstableContext } from '../src';
2+
import { render, act, fireEvent } from '@testing-library/react';
3+
import Mentions from '../src';
44
import { simulateInput } from './util';
55

66
describe('DropdownMenu', () => {
7-
// Generate 20 options for testing scrolling behavior
87
const generateOptions = Array.from({ length: 20 }).map((_, index) => ({
98
value: `item-${index}`,
109
label: `item-${index}`,
1110
}));
1211

12+
beforeAll(() => {
13+
global.ResizeObserver = class ResizeObserver {
14+
observe() {}
15+
unobserve() {}
16+
disconnect() {}
17+
};
18+
});
19+
1320
beforeEach(() => {
1421
jest.useFakeTimers();
1522
});
1623

1724
afterEach(() => {
1825
jest.useRealTimers();
26+
jest.clearAllMocks();
1927
});
2028

21-
it('should scroll into view when navigating with keyboard', async () => {
22-
// Setup component with UnstableContext for testing dropdown behavior
29+
it('should scroll into view when navigating with keyboard (Focused)', async () => {
2330
const { container } = render(
24-
<UnstableContext.Provider value={{ open: true }}>
25-
<Mentions defaultValue="@" options={generateOptions} />
26-
</UnstableContext.Provider>,
31+
<Mentions defaultValue="" options={generateOptions} />,
2732
);
2833

29-
// Mock scrollIntoView since it's not implemented in JSDOM
34+
const textarea = container.querySelector('textarea');
3035
const scrollIntoViewMock = jest
3136
.spyOn(HTMLElement.prototype, 'scrollIntoView')
3237
.mockImplementation(jest.fn());
3338

34-
// Trigger should not scroll
35-
simulateInput(container, '@');
36-
expect(scrollIntoViewMock).not.toHaveBeenCalled();
39+
await act(async () => {
40+
textarea.focus();
41+
42+
simulateInput(container, '@');
3743

38-
for (let i = 0; i < 10; i++) {
39-
await act(async () => {
40-
jest.advanceTimersByTime(1000);
41-
await Promise.resolve();
42-
});
43-
}
44+
jest.runAllTimers();
45+
});
46+
47+
await act(async () => {
48+
fireEvent.keyDown(textarea, { key: 'ArrowDown', code: 'ArrowDown' });
49+
jest.runAllTimers();
50+
});
4451

45-
// Verify if scrollIntoView was called
4652
expect(scrollIntoViewMock).toHaveBeenCalledWith({
4753
block: 'nearest',
4854
inline: 'nearest',
4955
});
5056

5157
scrollIntoViewMock.mockRestore();
5258
});
59+
60+
it('should NOT scroll into view when input is NOT focused', async () => {
61+
const { container } = render(
62+
<Mentions defaultValue="" options={generateOptions} />,
63+
);
64+
65+
const textarea = container.querySelector('textarea');
66+
const scrollIntoViewMock = jest
67+
.spyOn(HTMLElement.prototype, 'scrollIntoView')
68+
.mockImplementation(jest.fn());
69+
70+
await act(async () => {
71+
textarea.focus();
72+
simulateInput(container, '@');
73+
jest.runAllTimers();
74+
});
75+
76+
scrollIntoViewMock.mockClear();
77+
78+
await act(async () => {
79+
fireEvent.blur(textarea);
80+
jest.runAllTimers();
81+
});
82+
83+
await act(async () => {
84+
const menuItems = document.querySelectorAll('.rc-mentions-menu-item');
85+
if (menuItems.length > 1) {
86+
fireEvent.mouseEnter(menuItems[1]);
87+
jest.runAllTimers();
88+
}
89+
});
90+
91+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
92+
93+
scrollIntoViewMock.mockRestore();
94+
});
5395
});

0 commit comments

Comments
 (0)