Skip to content

Commit 9cc572e

Browse files
zombieJclaudegemini-code-assist[bot]
authored
fix(focus): add ignoreElement support and update deps (#557)
* fix(focus): add ignoreElement support and update deps - Update @rc-component/util to 1.9.0 - Add ignoreElement to handle focus elements from Portal - Remove console.log in Panel component - Add test case for focus handling with Portal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update tests/focus.spec.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 167d88f commit 9cc572e

File tree

5 files changed

+96
-4
lines changed

5 files changed

+96
-4
lines changed

docs/demo/with-portal.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: with-portal
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/with-portal.tsx"></code>

docs/examples/with-portal.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import Dialog from '@rc-component/dialog';
4+
5+
const DivPortal: React.FC = () => {
6+
return ReactDOM.createPortal(
7+
<div
8+
id="test-portal"
9+
style={{
10+
position: 'fixed',
11+
right: 20,
12+
bottom: 20,
13+
background: 'white',
14+
padding: 20,
15+
border: '1px solid #ccc',
16+
zIndex: 1000000,
17+
}}
18+
>
19+
<input type="text" />
20+
</div>,
21+
document.body
22+
);
23+
};
24+
25+
const MyControl: React.FC = () => {
26+
const [visible, setVisible] = React.useState(false);
27+
28+
const onClick = () => {
29+
setVisible(true);
30+
};
31+
32+
const onClose = () => {
33+
setVisible(false);
34+
};
35+
36+
return (
37+
<div style={{ margin: 20 }}>
38+
<p>
39+
<button type="button" onClick={onClick}>
40+
show dialog
41+
</button>
42+
</p>
43+
<Dialog visible={visible} onClose={onClose}>
44+
hello world
45+
<input type="text" />
46+
<input type="text" />
47+
<DivPortal />
48+
</Dialog>
49+
</div>
50+
);
51+
};
52+
53+
export default MyControl;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"dependencies": {
5252
"@rc-component/motion": "^1.1.3",
5353
"@rc-component/portal": "^2.1.0",
54-
"@rc-component/util": "^1.7.0",
54+
"@rc-component/util": "^1.9.0",
5555
"clsx": "^2.1.1"
5656
},
5757
"devDependencies": {
@@ -87,4 +87,4 @@
8787
"react": ">=18.0.0",
8888
"react-dom": ">=18.0.0"
8989
}
90-
}
90+
}

src/Dialog/Content/Panel.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ const Panel = React.forwardRef<PanelRef, PanelProps>((props, ref) => {
5454
const internalRef = useRef<HTMLDivElement>(null);
5555
const mergedRef = useComposeRef(holderRef, panelRef, internalRef);
5656

57-
useLockFocus(visible && isFixedPos && focusTrap !== false, () => internalRef.current);
57+
const [ignoreElement] = useLockFocus(
58+
visible && isFixedPos && focusTrap !== false,
59+
() => internalRef.current,
60+
);
5861

5962
React.useImperativeHandle(ref, () => ({
6063
focus: () => {
@@ -152,6 +155,9 @@ const Panel = React.forwardRef<PanelRef, PanelProps>((props, ref) => {
152155
onMouseDown={onMouseDown}
153156
onMouseUp={onMouseUp}
154157
tabIndex={-1}
158+
onFocus={(e) => {
159+
ignoreElement(e.target);
160+
}}
155161
>
156162
<MemoChildren shouldUpdate={visible || forceRender}>
157163
{modalRender ? modalRender(content) : content}

tests/focus.spec.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */
22
import React from 'react';
3+
import ReactDOM from 'react-dom';
34
import { act, render } from '@testing-library/react';
45
import Dialog from '../src';
56

@@ -9,7 +10,12 @@ jest.mock('@rc-component/util/lib/Dom/focus', () => {
910

1011
const useLockFocus = (visible: boolean, ...rest: any[]) => {
1112
globalThis.__useLockFocusVisible = visible;
12-
return actual.useLockFocus(visible, ...rest);
13+
const hooks = actual.useLockFocus(visible, ...rest);
14+
const proxyIgnoreElement = (ele: HTMLElement) => {
15+
globalThis.__ignoredElement = ele;
16+
hooks[0](ele);
17+
};
18+
return [proxyIgnoreElement, ...hooks.slice(1)] as ReturnType<typeof actual.useLockFocus>;
1319
};
1420

1521
return {
@@ -82,4 +88,23 @@ describe('Dialog.Focus', () => {
8288

8389
expect(globalThis.__useLockFocusVisible).toBe(false);
8490
});
91+
92+
it('should call ignoreElement when input in portal is focused', () => {
93+
render(
94+
<Dialog visible styles={{ wrapper: { position: 'fixed' } }}>
95+
{ReactDOM.createPortal(<input id="portal-input" />, document.body)}
96+
</Dialog>,
97+
);
98+
99+
act(() => {
100+
jest.runAllTimers();
101+
});
102+
103+
const input = document.getElementById('portal-input') as HTMLElement;
104+
act(() => {
105+
input.focus();
106+
});
107+
108+
expect(globalThis.__ignoredElement).toBe(input);
109+
});
85110
});

0 commit comments

Comments
 (0)