Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions src/Preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ const Preview: React.FC<PreviewProps> = props => {

const imgRef = useRef<HTMLImageElement>();
const wrapperRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement>(null);

const groupContext = useContext(PreviewGroupContext);
const showLeftOrRightSwitches = groupContext && count > 1;
const showOperationsProgress = groupContext && count >= 1;
Expand Down Expand Up @@ -366,6 +368,10 @@ const Preview: React.FC<PreviewProps> = 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);
};
Expand All @@ -385,6 +391,12 @@ const Preview: React.FC<PreviewProps> = props => {
};

// =========================== Focus ============================
useLayoutEffect(() => {
if (open) {
triggerRef.current = document.activeElement as HTMLElement;
}
}, [open]);

useLockFocus(open && portalRender, () => wrapperRef.current);

// ========================== Render ==========================
Expand Down
40 changes: 40 additions & 0 deletions tests/preview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Image src="src" alt="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();
});
});
Loading