Skip to content

Commit d1d22b3

Browse files
zombieJclaude
andcommitted
fix(focus): add ignoreElement support and update deps (#557)
- Update @rc-component/util to 1.9.0 - Add ignoreElement to handle focus elements from Portal - Add onClose and onFocus type to DrawerPanelEvents - Add test case for focus handling with Portal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 925f9f5 commit d1d22b3

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"dependencies": {
4646
"@rc-component/motion": "^1.1.4",
4747
"@rc-component/portal": "^2.1.3",
48-
"@rc-component/util": "^1.2.1",
48+
"@rc-component/util": "^1.9.0",
4949
"clsx": "^2.1.1"
5050
},
5151
"devDependencies": {

src/DrawerPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface DrawerPanelEvents {
1515
onClick?: React.MouseEventHandler<HTMLDivElement>;
1616
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
1717
onKeyUp?: React.KeyboardEventHandler<HTMLDivElement>;
18+
onFocus?: React.FocusEventHandler<HTMLDivElement>;
1819
}
1920

2021
export type DrawerPanelAccessibility = Pick<
@@ -23,8 +24,7 @@ export type DrawerPanelAccessibility = Pick<
2324
>;
2425

2526
export interface DrawerPanelProps
26-
extends DrawerPanelEvents,
27-
DrawerPanelAccessibility {
27+
extends DrawerPanelEvents, DrawerPanelAccessibility {
2828
prefixCls: string;
2929
className?: string;
3030
id?: string;

src/DrawerPopup.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ const DrawerPopup: React.ForwardRefRenderFunction<
151151
React.useImperativeHandle(ref, () => panelRef.current);
152152

153153
// ========================= Focusable ==========================
154-
useFocusable(() => panelRef.current, open, autoFocus, focusTrap, mask);
154+
const ignoreElement = useFocusable(
155+
() => panelRef.current,
156+
open,
157+
autoFocus,
158+
focusTrap,
159+
mask,
160+
);
155161

156162
// ============================ Push ============================
157163
const [pushed, setPushed] = React.useState(false);
@@ -305,6 +311,9 @@ const DrawerPopup: React.ForwardRefRenderFunction<
305311
onClick,
306312
onKeyDown,
307313
onKeyUp,
314+
onFocus: (e: React.FocusEvent<HTMLDivElement>) => {
315+
ignoreElement(e.target);
316+
},
308317
};
309318

310319
// =========================== Render ==========================

src/hooks/useFocusable.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ export default function useFocusable(
1111
const mergedFocusTrap = focusTrap ?? mask !== false;
1212

1313
// Focus lock
14-
useLockFocus(open && mergedFocusTrap, getContainer);
14+
const [ignoreElement] = useLockFocus(open && mergedFocusTrap, getContainer);
1515

1616
// Auto Focus
1717
React.useEffect(() => {
1818
if (open && autoFocus === true) {
1919
getContainer()?.focus({ preventScroll: true });
2020
}
2121
}, [open]);
22+
23+
return ignoreElement;
2224
}

tests/focus.spec.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { cleanup, render, act } from '@testing-library/react';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom';
4+
import Drawer from '../src';
5+
6+
// Mock useLockFocus to track calls
7+
jest.mock('@rc-component/util/lib/Dom/focus', () => {
8+
const actual = jest.requireActual('@rc-component/util/lib/Dom/focus');
9+
10+
const useLockFocus = (visible: boolean, ...rest: any[]) => {
11+
(globalThis as any).__useLockFocusVisible = visible;
12+
const hooks = actual.useLockFocus(visible, ...rest);
13+
const hooksArray = Array.isArray(hooks) ? hooks : [hooks];
14+
const proxyIgnoreElement = (ele: HTMLElement) => {
15+
(globalThis as any).__ignoredElement = ele;
16+
hooksArray[0](ele);
17+
};
18+
return [proxyIgnoreElement, ...hooksArray.slice(1)] as ReturnType<
19+
typeof actual.useLockFocus
20+
>;
21+
};
22+
23+
return {
24+
...actual,
25+
useLockFocus,
26+
};
27+
});
28+
29+
describe('Drawer.Focus', () => {
30+
beforeEach(() => {
31+
jest.useFakeTimers();
32+
});
33+
34+
afterEach(() => {
35+
jest.useRealTimers();
36+
cleanup();
37+
});
38+
39+
it('should call ignoreElement when input in portal is focused', () => {
40+
render(
41+
<Drawer open>
42+
<input type="text" id="drawer-input" />
43+
{ReactDOM.createPortal(
44+
<input type="text" id="portal-input" />,
45+
document.body,
46+
)}
47+
</Drawer>,
48+
);
49+
50+
act(() => {
51+
jest.runAllTimers();
52+
});
53+
54+
const input = document.getElementById('portal-input') as HTMLElement;
55+
act(() => {
56+
input.focus();
57+
});
58+
59+
expect((globalThis as any).__ignoredElement).toBe(input);
60+
});
61+
});

0 commit comments

Comments
 (0)