diff --git a/package.json b/package.json index 18ce3fc..b7e3f6b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", - "@rc-component/util": "^1.7.0", + "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "devDependencies": { diff --git a/src/DrawerPanel.tsx b/src/DrawerPanel.tsx index 29c0521..9066281 100644 --- a/src/DrawerPanel.tsx +++ b/src/DrawerPanel.tsx @@ -15,6 +15,7 @@ export interface DrawerPanelEvents { onClick?: React.MouseEventHandler; onKeyDown?: React.KeyboardEventHandler; onKeyUp?: React.KeyboardEventHandler; + onFocus?: React.FocusEventHandler; } export type DrawerPanelAccessibility = Pick< @@ -23,8 +24,7 @@ export type DrawerPanelAccessibility = Pick< >; export interface DrawerPanelProps - extends DrawerPanelEvents, - DrawerPanelAccessibility { + extends DrawerPanelEvents, DrawerPanelAccessibility { prefixCls: string; className?: string; id?: string; diff --git a/src/DrawerPopup.tsx b/src/DrawerPopup.tsx index c41dbd4..eeb39e5 100644 --- a/src/DrawerPopup.tsx +++ b/src/DrawerPopup.tsx @@ -151,7 +151,13 @@ const DrawerPopup: React.ForwardRefRenderFunction< React.useImperativeHandle(ref, () => panelRef.current); // ========================= Focusable ========================== - useFocusable(() => panelRef.current, open, autoFocus, focusTrap, mask); + const ignoreElement = useFocusable( + () => panelRef.current, + open, + autoFocus, + focusTrap, + mask, + ); // ============================ Push ============================ const [pushed, setPushed] = React.useState(false); @@ -305,6 +311,9 @@ const DrawerPopup: React.ForwardRefRenderFunction< onClick, onKeyDown, onKeyUp, + onFocus: (e: React.FocusEvent) => { + ignoreElement(e.target); + }, }; // =========================== Render ========================== diff --git a/src/hooks/useFocusable.ts b/src/hooks/useFocusable.ts index 8a06ca0..9a4b8d8 100644 --- a/src/hooks/useFocusable.ts +++ b/src/hooks/useFocusable.ts @@ -11,7 +11,7 @@ export default function useFocusable( const mergedFocusTrap = focusTrap ?? mask !== false; // Focus lock - useLockFocus(open && mergedFocusTrap, getContainer); + const [ignoreElement] = useLockFocus(open && mergedFocusTrap, getContainer); // Auto Focus React.useEffect(() => { @@ -19,4 +19,6 @@ export default function useFocusable( getContainer()?.focus({ preventScroll: true }); } }, [open]); + + return ignoreElement; } diff --git a/tests/focus.spec.tsx b/tests/focus.spec.tsx new file mode 100644 index 0000000..0847e85 --- /dev/null +++ b/tests/focus.spec.tsx @@ -0,0 +1,61 @@ +import { cleanup, render, act } from '@testing-library/react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import Drawer from '../src'; + +// Mock useLockFocus to track calls +jest.mock('@rc-component/util/lib/Dom/focus', () => { + const actual = jest.requireActual('@rc-component/util/lib/Dom/focus'); + + const useLockFocus = (visible: boolean, ...rest: any[]) => { + (globalThis as any).__useLockFocusVisible = visible; + const hooks = actual.useLockFocus(visible, ...rest); + const hooksArray = Array.isArray(hooks) ? hooks : [hooks]; + const proxyIgnoreElement = (ele: HTMLElement) => { + (globalThis as any).__ignoredElement = ele; + hooksArray[0](ele); + }; + return [proxyIgnoreElement, ...hooksArray.slice(1)] as ReturnType< + typeof actual.useLockFocus + >; + }; + + return { + ...actual, + useLockFocus, + }; +}); + +describe('Drawer.Focus', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + cleanup(); + }); + + it('should call ignoreElement when input in portal is focused', () => { + render( + + + {ReactDOM.createPortal( + , + document.body, + )} + , + ); + + act(() => { + jest.runAllTimers(); + }); + + const input = document.getElementById('portal-input') as HTMLElement; + act(() => { + input.focus(); + }); + + expect((globalThis as any).__ignoredElement).toBe(input); + }); +});