Skip to content

Commit 2dc503e

Browse files
authored
chore: lock only when fixed (#530)
* chore: safe lock of content * chore: lock only when fixed
1 parent 2e12623 commit 2dc503e

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed

src/Dialog/Content/Panel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface PanelProps extends Omit<IDialogPropTypes, 'getOpenCount'> {
1313
onMouseDown?: React.MouseEventHandler;
1414
onMouseUp?: React.MouseEventHandler;
1515
holderRef?: React.Ref<HTMLDivElement>;
16+
/** Used for focus lock. When true and open, focus will lock into the panel */
17+
isFixedPos?: boolean;
1618
}
1719

1820
export type PanelRef = {
@@ -43,14 +45,15 @@ const Panel = React.forwardRef<PanelRef, PanelProps>((props, ref) => {
4345
height,
4446
classNames: modalClassNames,
4547
styles: modalStyles,
48+
isFixedPos,
4649
} = props;
4750

4851
// ================================= Refs =================================
4952
const { panel: panelRef } = React.useContext(RefContext);
5053
const internalRef = useRef<HTMLDivElement>(null);
5154
const mergedRef = useComposeRef(holderRef, panelRef, internalRef);
5255

53-
useLockFocus(visible, () => internalRef.current);
56+
useLockFocus(visible && isFixedPos, () => internalRef.current);
5457

5558
React.useImperativeHandle(ref, () => ({
5659
focus: () => {

src/Dialog/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const Dialog: React.FC<IDialogPropTypes> = (props) => {
5959
const contentRef = useRef<ContentRef>(null);
6060

6161
const [animatedVisible, setAnimatedVisible] = React.useState(visible);
62+
const [isFixedPos, setIsFixedPos] = React.useState(false);
6263

6364
// ========================== Init ==========================
6465
const ariaId = useId();
@@ -116,7 +117,7 @@ const Dialog: React.FC<IDialogPropTypes> = (props) => {
116117
const contentClickRef = useRef(false);
117118
const contentTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
118119

119-
// We need record content click incase content popup out of dialog
120+
// We need record content click in case content popup out of dialog
120121
const onContentMouseDown: React.MouseEventHandler = () => {
121122
clearTimeout(contentTimeoutRef.current);
122123
contentClickRef.current = true;
@@ -154,6 +155,12 @@ const Dialog: React.FC<IDialogPropTypes> = (props) => {
154155
if (visible) {
155156
setAnimatedVisible(true);
156157
saveLastOutSideActiveElementRef();
158+
159+
// Calc the position style
160+
if (wrapperRef.current) {
161+
const computedWrapStyle = getComputedStyle(wrapperRef.current);
162+
setIsFixedPos(computedWrapStyle.position === 'fixed');
163+
}
157164
} else if (
158165
animatedVisible &&
159166
contentRef.current.enableMotion() &&
@@ -203,6 +210,7 @@ const Dialog: React.FC<IDialogPropTypes> = (props) => {
203210
>
204211
<Content
205212
{...props}
213+
isFixedPos={isFixedPos}
206214
onMouseDown={onContentMouseDown}
207215
onMouseUp={onContentMouseUp}
208216
ref={contentRef}

tests/focus.spec.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */
2+
import React from 'react';
3+
import { act, render } from '@testing-library/react';
4+
import Dialog from '../src';
5+
6+
// Mock: import { useLockFocus } from '@rc-component/util/lib/Dom/focus';
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.__useLockFocusVisible = visible;
12+
return actual.useLockFocus(visible, ...rest);
13+
};
14+
15+
return {
16+
...actual,
17+
useLockFocus,
18+
};
19+
});
20+
21+
/**
22+
* Since overflow scroll test need a clear env which may affect by other test.
23+
* Use a clean env instead.
24+
*/
25+
describe('Dialog.Focus', () => {
26+
beforeEach(() => {
27+
jest.useFakeTimers();
28+
});
29+
30+
afterEach(() => {
31+
jest.useRealTimers();
32+
});
33+
34+
it('Should lock when fixed', () => {
35+
render(
36+
<Dialog
37+
visible
38+
styles={{
39+
wrapper: { position: 'fixed' },
40+
}}
41+
/>,
42+
);
43+
44+
act(() => {
45+
jest.runAllTimers();
46+
});
47+
48+
expect(globalThis.__useLockFocusVisible).toBe(true);
49+
});
50+
51+
it('Should not lock when not fixed', () => {
52+
render(
53+
<Dialog
54+
visible
55+
styles={{
56+
wrapper: { position: 'absolute' },
57+
}}
58+
/>,
59+
);
60+
61+
act(() => {
62+
jest.runAllTimers();
63+
});
64+
65+
expect(globalThis.__useLockFocusVisible).toBe(false);
66+
});
67+
});

0 commit comments

Comments
 (0)