diff --git a/package.json b/package.json index d03def8..bb3d984 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", - "@rc-component/util": "^1.10.0", + "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "devDependencies": { diff --git a/src/Preview/index.tsx b/src/Preview/index.tsx index 94b76ad..065fdb1 100644 --- a/src/Preview/index.tsx +++ b/src/Preview/index.tsx @@ -197,6 +197,8 @@ const Preview: React.FC = props => { const imgRef = useRef(); const wrapperRef = useRef(null); + const triggerRef = useRef(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,6 +391,12 @@ const Preview: React.FC = props => { }; // =========================== Focus ============================ + useLayoutEffect(() => { + if (open) { + triggerRef.current = document.activeElement as HTMLElement; + } + }, [open]); + useLockFocus(open && portalRender, () => wrapperRef.current); // ========================== Render ========================== diff --git a/tests/preview.test.tsx b/tests/preview.test.tsx index 091a1c1..f81744c 100644 --- a/tests/preview.test.tsx +++ b/tests/preview.test.tsx @@ -1256,4 +1256,44 @@ describe('Preview', () => { 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(); + }); });