Skip to content

Commit ef83ba6

Browse files
committed
WIP
1 parent 8ce16cb commit ef83ba6

File tree

3 files changed

+72
-86
lines changed

3 files changed

+72
-86
lines changed

src/Modal.tsx

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
} from 'react';
1313
import * as React from 'react';
1414
import ReactDOM from 'react-dom';
15-
import useMounted from '@restart/hooks/useMounted';
1615
import useWillUnmount from '@restart/hooks/useWillUnmount';
1716

1817
import usePrevious from '@restart/hooks/usePrevious';
@@ -21,6 +20,7 @@ import ModalManager from './ModalManager.js';
2120
import useWaitForDOMRef, { type DOMContainer } from './useWaitForDOMRef.js';
2221
import type { TransitionCallbacks, TransitionComponent } from './types.js';
2322
import useWindow from './useWindow.js';
23+
import { useFocusTrap } from './useFocusTrap.js';
2424
import {
2525
renderTransition,
2626
type TransitionHandler,
@@ -298,11 +298,16 @@ const Modal: React.ForwardRefExoticComponent<
298298
const container = useWaitForDOMRef(containerRef);
299299
const modal = useModalManager(providedManager);
300300

301-
const isMounted = useMounted();
302301
const prevShow = usePrevious(show);
303302
const [exited, setExited] = useState(!show);
303+
const removeKeydownListenerRef = useRef<(() => void) | null>(null);
304304
const lastFocusRef = useRef<HTMLElement | null>(null);
305305

306+
const focusTrap = useFocusTrap({
307+
container: modal.dialog,
308+
disabled: () => !enforceFocus || !modal.isTopModal(),
309+
});
310+
306311
useImperativeHandle(ref, () => modal, [modal]);
307312

308313
if (canUseDOM && !prevShow && show) {
@@ -325,14 +330,7 @@ const Modal: React.ForwardRefExoticComponent<
325330
handleDocumentKeyDown,
326331
);
327332

328-
removeFocusListenerRef.current = listen(
329-
document as any,
330-
'focus',
331-
// the timeout is necessary b/c this will run before the new modal is mounted
332-
// and so steals focus from it
333-
() => setTimeout(handleEnforceFocus),
334-
true,
335-
);
333+
focusTrap.start();
336334

337335
if (onShow) {
338336
onShow();
@@ -360,7 +358,7 @@ const Modal: React.ForwardRefExoticComponent<
360358
modal.remove();
361359

362360
removeKeydownListenerRef.current?.();
363-
removeFocusListenerRef.current?.();
361+
focusTrap.stop();
364362

365363
if (restoreFocus) {
366364
// Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917)
@@ -394,22 +392,6 @@ const Modal: React.ForwardRefExoticComponent<
394392

395393
// --------------------------------
396394

397-
const handleEnforceFocus = useEventCallback(() => {
398-
if (!enforceFocus || !isMounted() || !modal.isTopModal()) {
399-
return;
400-
}
401-
402-
const currentActiveElement = activeElement(ownerWindow?.document);
403-
404-
if (
405-
modal.dialog &&
406-
currentActiveElement &&
407-
!contains(modal.dialog, currentActiveElement)
408-
) {
409-
modal.dialog.focus();
410-
}
411-
});
412-
413395
const handleBackdropClick = useEventCallback((e: React.SyntheticEvent) => {
414396
if (e.target !== e.currentTarget) {
415397
return;
@@ -432,13 +414,6 @@ const Modal: React.ForwardRefExoticComponent<
432414
}
433415
});
434416

435-
const removeFocusListenerRef = useRef<ReturnType<typeof listen> | null>(
436-
null,
437-
);
438-
const removeKeydownListenerRef = useRef<ReturnType<typeof listen> | null>(
439-
null,
440-
);
441-
442417
const handleHidden: TransitionCallbacks['onExited'] = (...args) => {
443418
setExited(true);
444419
onExited?.(...args);

src/useFocusTrap.tsx

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,63 @@ export function useFocusTrap({
1111
disabled,
1212
}: {
1313
container: HTMLElement;
14-
disabled?: boolean;
14+
disabled?: () => boolean;
1515
}) {
1616
const ownerWindow = useWindow();
1717
const isMounted = useMounted();
1818

1919
const listenersRef = useRef(new Set<(...args: any[]) => void>());
2020

21+
const handleKeydown = useEventCallback((event: KeyboardEvent) => {
22+
if (event.key !== 'Tab' || !container) {
23+
return;
24+
}
25+
26+
const tabbables = getTabbableElements(container);
27+
const currentActiveElement = activeElement(ownerWindow?.document);
28+
29+
const isTabbingBackwards =
30+
currentActiveElement === tabbables[0] && event.shiftKey;
31+
32+
const isTabbingForward =
33+
currentActiveElement === tabbables[tabbables.length - 1];
34+
35+
if (!isTabbingBackwards && !isTabbingForward) {
36+
return;
37+
}
38+
39+
/**
40+
* We want focus to move from the focus trapped container out of the document like
41+
* it would if you tabbed from the end or start of the page.
42+
*
43+
* Generally this Just Works for tabbing forward out of the modal, as modals are often
44+
* the last element in the document. In cases where it isn't or if you tabbed backwards
45+
* we need to allow focus to move out of the document.
46+
*
47+
* This is done by way of a little "trick". `tab` events happen before focus moves so
48+
* you can shift the focus to a new element and the tab will then move focus forward or backwards
49+
* depending on the direction. We take advantage of this by moving focus to the first or last tabbable element
50+
* in the document.
51+
*/
52+
const bodyTabbables = getTabbableElements(
53+
ownerWindow?.document ?? document!,
54+
);
55+
56+
if (isTabbingBackwards) {
57+
if (bodyTabbables[0] !== currentActiveElement) {
58+
bodyTabbables[0]?.focus();
59+
}
60+
} else if (isTabbingForward) {
61+
const lastTabbable = bodyTabbables[bodyTabbables.length - 1];
62+
63+
if (lastTabbable !== currentActiveElement) {
64+
lastTabbable?.focus();
65+
}
66+
}
67+
});
68+
2169
const handleEnforceFocus = useEventCallback((_event: FocusEvent) => {
22-
if (disabled) {
70+
if (disabled?.()) {
2371
return;
2472
}
2573

@@ -30,68 +78,31 @@ export function useFocusTrap({
3078
currentActiveElement &&
3179
!container.contains(currentActiveElement)
3280
) {
33-
container.focus();
81+
const tabbables = getTabbableElements(container);
82+
83+
(tabbables[0] ?? container).focus();
3484
}
3585
});
3686

3787
const start = useCallback(() => {
3888
const document = ownerWindow?.document;
3989

40-
if (!document || !isMounted()) {
90+
if (!ownerWindow || !document || !isMounted()) {
4191
return;
4292
}
4393

44-
document.addEventListener('focus', handleFocus, { capture: true });
94+
ownerWindow.addEventListener('focus', handleFocus, { capture: true });
4595
document.addEventListener('keydown', handleKeydown);
4696

47-
listenersRef.current.add(handleFocus);
48-
listenersRef.current.add(handleKeydown);
49-
50-
function handleKeydown(event: KeyboardEvent) {
51-
const tabbables = getTabbableElements(container);
52-
const currentActiveElement = activeElement(ownerWindow?.document);
53-
54-
const isTabbingBackwards =
55-
currentActiveElement === tabbables[0] && event.shiftKey;
56-
57-
const isTabbingForward =
58-
currentActiveElement === tabbables[tabbables.length - 1];
59-
60-
if (!isTabbingBackwards && !isTabbingForward) {
61-
return;
62-
}
63-
64-
/**
65-
* We want focus to move from the focus trapped container out of the document like
66-
* it would if you tabbed from the end or start of the page.
67-
*
68-
* Generally this Just Works for tabbing forward out of the modal, as modals are often
69-
* the last element in the document. In cases where it isn't or if you tabbed backwards
70-
* we need to allow focus to move out of the document.
71-
*
72-
* This is done by way of a little "trick". `tab` events happen before focus moves so
73-
* you can shift the focus to a new element and the tab will then move focus forward or backwards
74-
* depending on the direction. We take advantage of this by moving focus to the first or last tabbable element
75-
* in the document.
76-
*/
77-
const bodyTabbables = getTabbableElements(
78-
ownerWindow?.document ?? document!,
79-
);
80-
81-
if (isTabbingBackwards) {
82-
if (bodyTabbables[0] !== currentActiveElement) {
83-
bodyTabbables[0]?.focus();
84-
}
85-
} else if (isTabbingForward) {
86-
const lastTabbable = bodyTabbables[bodyTabbables.length - 1];
87-
88-
if (lastTabbable !== currentActiveElement) {
89-
lastTabbable?.focus();
90-
}
91-
}
92-
}
97+
listenersRef.current.add(() =>
98+
ownerWindow.removeEventListener('focus', handleFocus, { capture: true }),
99+
);
100+
listenersRef.current.add(() =>
101+
document.removeEventListener('keydown', handleKeydown),
102+
);
93103

94104
function handleFocus(event: FocusEvent) {
105+
console.log('handleFocus', event.target);
95106
// the timeout is necessary b/c this will run before the new modal is mounted
96107
// and so steals focus from it
97108
setTimeout(() => handleEnforceFocus(event));

www/plugins/webpack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = () => ({
44
name: 'webpack-plugin',
55
configureWebpack(_, isServer, { getJSLoader }) {
66
return {
7-
devtool: 'inline-cheap-module-source-map',
7+
devtool: 'eval-source-map',
88

99
resolve: {
1010
alias: {

0 commit comments

Comments
 (0)