From 263ebe16666cc3e07c1e0e86be07d87b01cb158f Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Sat, 4 Apr 2026 03:21:18 +0800 Subject: [PATCH] fix: ensure focus is properly locked inside preview when opened via keyboard The preview wrapper DOM element is not immediately available due to CSSMotion's deferred rendering (styleReady='NONE' on first render). Using useRef meant useLockFocus could never re-evaluate after the DOM appeared. Switch to useState callback ref so the component re-renders once the wrapper mounts, allowing useLockFocus to activate correctly. Also restores focus to the trigger element after the preview closes. --- src/Preview/index.tsx | 18 +++++++++++++++--- tests/preview.test.tsx | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index 94b76ad..d99a48f 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -196,7 +196,9 @@ const Preview: React.FC = props => { } = props; const imgRef = useRef(); - const wrapperRef = useRef(null); + const triggerRef = useRef(null); + const [wrapperEl, setWrapperEl] = useState(null); + const groupContext = useContext(PreviewGroupContext); const showLeftOrRightSwitches = groupContext && count > 1; const showOperationsProgress = groupContext && count >= 1; @@ -366,6 +368,10 @@ const Preview: React.FC = props => { const onVisibleChanged = (nextVisible: boolean) => { if (!nextVisible) { setLockScroll(false); + + // Restore focus to the trigger element after leave animation + triggerRef.current?.focus?.(); + triggerRef.current = null; } afterOpenChange?.(nextVisible); }; @@ -385,7 +391,13 @@ const Preview: React.FC = props => { }; // =========================== Focus ============================ - useLockFocus(open && portalRender, () => wrapperRef.current); + useEffect(() => { + if (open) { + triggerRef.current = document.activeElement as HTMLElement; + } + }, [open]); + + useLockFocus(open && !!wrapperEl, () => wrapperEl); // ========================== Render ========================== const bodyStyle: React.CSSProperties = { @@ -423,7 +435,7 @@ const Preview: React.FC = props => { return (
{ expect(document.querySelector('.rc-image-preview')).toBeFalsy(); }); + + it('Focus should be trapped inside preview after keyboard open and restored on close', () => { + const rectSpy = jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ + x: 0, y: 0, width: 100, height: 100, + top: 0, right: 100, bottom: 100, left: 0, + toJSON: () => undefined, + } as DOMRect); + + const { container } = render(focus trap); + const wrapper = container.querySelector('.rc-image') as HTMLElement; + + // Open preview via keyboard + wrapper.focus(); + expect(document.activeElement).toBe(wrapper); + + fireEvent.keyDown(wrapper, { key: 'Enter' }); + act(() => { + jest.runAllTimers(); + }); + + // Focus should be inside the preview + const preview = document.querySelector('.rc-image-preview') as HTMLElement; + expect(preview).toBeTruthy(); + expect(preview.contains(document.activeElement)).toBeTruthy(); + + // Focus should not escape when trying to focus outside + wrapper.focus(); + expect(preview.contains(document.activeElement)).toBeTruthy(); + + // Close preview via Escape + fireEvent.keyDown(window, { key: 'Escape' }); + act(() => { + jest.runAllTimers(); + }); + + // Focus should return to the trigger element + expect(document.activeElement).toBe(wrapper); + + rectSpy.mockRestore(); + }); });