Skip to content

Commit 5c63523

Browse files
authored
feat: Support focusable (#561)
* chore: init * feat: support useFocusLock * chore: refactor pos * chore: clean up
1 parent 2886a76 commit 5c63523

File tree

8 files changed

+41
-91
lines changed

8 files changed

+41
-91
lines changed

assets/index.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
z-index: @zIndex;
3333
// overflow: hidden;
3434
transition: transform @duration;
35+
pointer-events: auto;
3536

3637
&-hidden {
3738
display: none;

docs/examples/base.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const Demo = () => {
2727
{...motionProps}
2828
>
2929
content
30+
<button>Button 1</button>
31+
<button>Button 2</button>
3032
</Drawer>
3133
<div>
3234
<button onClick={onSwitch}>打开</button>

docs/examples/motion.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const maskMotion: DrawerProps['maskMotion'] = {
1010
export const motion: DrawerProps['motion'] = placement => ({
1111
motionAppear: true,
1212
motionName: `panel-motion-${placement}`,
13+
motionDeadline: 1000,
1314
});
1415

1516
const motionProps: Partial<DrawerProps> = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"dependencies": {
4646
"@rc-component/motion": "^1.1.4",
47-
"@rc-component/portal": "^2.0.0",
47+
"@rc-component/portal": "^2.1.3",
4848
"@rc-component/util": "^1.2.1",
4949
"clsx": "^2.1.1"
5050
},

src/Drawer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface DrawerProps
4949
onResizeStart?: () => void;
5050
onResizeEnd?: () => void;
5151
};
52+
focusTriggerAfterClose?: boolean;
5253
}
5354

5455
const Drawer: React.FC<DrawerProps> = props => {
@@ -77,6 +78,7 @@ const Drawer: React.FC<DrawerProps> = props => {
7778
onClose,
7879
resizable,
7980
defaultSize,
81+
focusTriggerAfterClose,
8082

8183
// Refs
8284
panelRef,
@@ -116,6 +118,7 @@ const Drawer: React.FC<DrawerProps> = props => {
116118

117119
if (
118120
!nextVisible &&
121+
focusTriggerAfterClose !== false &&
119122
lastActiveRef.current &&
120123
!popupRef.current?.contains(lastActiveRef.current)
121124
) {

src/DrawerPopup.tsx

Lines changed: 11 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { clsx } from 'clsx';
22
import type { CSSMotionProps } from '@rc-component/motion';
33
import CSSMotion from '@rc-component/motion';
4-
import KeyCode from '@rc-component/util/lib/KeyCode';
54
import pickAttrs from '@rc-component/util/lib/pickAttrs';
65
import * as React from 'react';
76
import type { DrawerContextProps } from './context';
@@ -15,14 +14,7 @@ import useDrag from './hooks/useDrag';
1514
import { parseWidthHeight } from './util';
1615
import type { DrawerClassNames, DrawerStyles } from './inter';
1716
import { useEvent } from '@rc-component/util';
18-
19-
const sentinelStyle: React.CSSProperties = {
20-
width: 0,
21-
height: 0,
22-
overflow: 'hidden',
23-
outline: 'none',
24-
position: 'absolute',
25-
};
17+
import useFocusable from './hooks/useFocusable';
2618

2719
export type Placement = 'left' | 'right' | 'top' | 'bottom';
2820

@@ -37,9 +29,12 @@ export interface DrawerPopupProps
3729
inline?: boolean;
3830
push?: boolean | PushConfig;
3931
forceRender?: boolean;
40-
autoFocus?: boolean;
4132
keyboard?: boolean;
4233

34+
// Focus
35+
autoFocus?: boolean;
36+
focusTrap?: boolean;
37+
4338
// Root
4439
rootClassName?: string;
4540
rootStyle?: React.CSSProperties;
@@ -104,7 +99,10 @@ const DrawerPopup: React.ForwardRefRenderFunction<
10499
inline,
105100
push,
106101
forceRender,
102+
103+
// Focus
107104
autoFocus,
105+
focusTrap,
108106

109107
// classNames
110108
classNames: drawerClassNames,
@@ -149,39 +147,11 @@ const DrawerPopup: React.ForwardRefRenderFunction<
149147

150148
// ================================ Refs ================================
151149
const panelRef = React.useRef<HTMLDivElement>(null);
152-
const sentinelStartRef = React.useRef<HTMLDivElement>(null);
153-
const sentinelEndRef = React.useRef<HTMLDivElement>(null);
154150

155151
React.useImperativeHandle(ref, () => panelRef.current);
156152

157-
const onPanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
158-
const { keyCode, shiftKey } = event;
159-
160-
switch (keyCode) {
161-
// Tab active
162-
case KeyCode.TAB: {
163-
if (keyCode === KeyCode.TAB) {
164-
if (!shiftKey && document.activeElement === sentinelEndRef.current) {
165-
sentinelStartRef.current?.focus({ preventScroll: true });
166-
} else if (
167-
shiftKey &&
168-
document.activeElement === sentinelStartRef.current
169-
) {
170-
sentinelEndRef.current?.focus({ preventScroll: true });
171-
}
172-
}
173-
break;
174-
}
175-
}
176-
};
177-
178-
// ========================== Control ===========================
179-
// Auto Focus
180-
React.useEffect(() => {
181-
if (open && autoFocus) {
182-
panelRef.current?.focus({ preventScroll: true });
183-
}
184-
}, [open]);
153+
// ========================= Focusable ==========================
154+
useFocusable(() => panelRef.current, open, autoFocus, focusTrap, mask);
185155

186156
// ============================ Push ============================
187157
const [pushed, setPushed] = React.useState(false);
@@ -345,9 +315,7 @@ const DrawerPopup: React.ForwardRefRenderFunction<
345315
{...motionProps}
346316
visible={open}
347317
forceRender={forceRender}
348-
onVisibleChanged={nextVisible => {
349-
afterOpenChange?.(nextVisible);
350-
}}
318+
onVisibleChanged={afterOpenChange}
351319
removeOnLeave={false}
352320
leavedClassName={`${prefixCls}-content-wrapper-hidden`}
353321
>
@@ -404,24 +372,9 @@ const DrawerPopup: React.ForwardRefRenderFunction<
404372
style={containerStyle}
405373
tabIndex={-1}
406374
ref={panelRef}
407-
onKeyDown={onPanelKeyDown}
408375
>
409376
{maskNode}
410-
<div
411-
tabIndex={0}
412-
ref={sentinelStartRef}
413-
style={sentinelStyle}
414-
aria-hidden="true"
415-
data-sentinel="start"
416-
/>
417377
{panelNode}
418-
<div
419-
tabIndex={0}
420-
ref={sentinelEndRef}
421-
style={sentinelStyle}
422-
aria-hidden="true"
423-
data-sentinel="end"
424-
/>
425378
</div>
426379
</DrawerContext.Provider>
427380
);

src/hooks/useFocusable.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { useLockFocus } from '@rc-component/util/lib/Dom/focus';
3+
4+
export default function useFocusable(
5+
getContainer: () => HTMLElement,
6+
open: boolean,
7+
autoFocus?: boolean,
8+
focusTrap?: boolean,
9+
mask?: boolean,
10+
) {
11+
const mergedFocusTrap = focusTrap ?? mask !== false;
12+
13+
// Focus lock
14+
useLockFocus(open && mergedFocusTrap, getContainer);
15+
16+
// Auto Focus
17+
React.useEffect(() => {
18+
if (open && autoFocus === true) {
19+
getContainer()?.focus({ preventScroll: true });
20+
}
21+
}, [open]);
22+
}

tests/index.spec.tsx

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { cleanup, fireEvent, render, act } from '@testing-library/react';
2-
import KeyCode from '@rc-component/util/lib/KeyCode';
32
import { resetWarned } from '@rc-component/util/lib/warning';
43
import React from 'react';
54
import type { DrawerProps } from '../src';
@@ -265,37 +264,6 @@ describe('rc-drawer-menu', () => {
265264
expect(container.contains(document.activeElement)).toBeTruthy();
266265
});
267266

268-
it('tab should always in the content', () => {
269-
const { container } = render(
270-
<Drawer open getContainer={false}>
271-
<div>Hello World</div>
272-
</Drawer>,
273-
);
274-
275-
const firstSentinel = container.querySelector<HTMLElement>(
276-
'[data-sentinel="start"]',
277-
);
278-
const lastSentinel = container.querySelector<HTMLElement>(
279-
'[data-sentinel="end"]',
280-
);
281-
282-
// First shift to last
283-
firstSentinel.focus();
284-
fireEvent.keyDown(firstSentinel, {
285-
shiftKey: true,
286-
keyCode: KeyCode.TAB,
287-
which: KeyCode.TAB,
288-
});
289-
expect(document.activeElement).toBe(lastSentinel);
290-
291-
// Last tab to first
292-
fireEvent.keyDown(lastSentinel, {
293-
keyCode: KeyCode.TAB,
294-
which: KeyCode.TAB,
295-
});
296-
expect(document.activeElement).toBe(firstSentinel);
297-
});
298-
299267
describe('keyboard', () => {
300268
it('ESC to exit', () => {
301269
const onClose = jest.fn();

0 commit comments

Comments
 (0)