Skip to content

Commit ebc299f

Browse files
authored
[react-interactions] TabFocus -> FocusManager (#16874)
1 parent 793f176 commit ebc299f

File tree

6 files changed

+242
-111
lines changed

6 files changed

+242
-111
lines changed

packages/react-interactions/accessibility/tab-focus.js renamed to packages/react-interactions/accessibility/focus-manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99

1010
'use strict';
1111

12-
module.exports = require('./src/TabFocus');
12+
module.exports = require('./src/FocusManager');
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactScope} from 'shared/ReactTypes';
11+
import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12+
13+
import React from 'react';
14+
import {useKeyboard} from 'react-interactions/events/keyboard';
15+
import {useFocusWithin} from 'react-interactions/events/focus';
16+
import {
17+
focusFirst,
18+
focusPrevious,
19+
focusNext,
20+
} from 'react-interactions/accessibility/focus-control';
21+
import TabbableScope from 'react-interactions/accessibility/tabbable-scope';
22+
23+
type TabFocusProps = {|
24+
autoFocus?: boolean,
25+
children: React.Node,
26+
containFocus?: boolean,
27+
restoreFocus?: boolean,
28+
scope: ReactScope,
29+
|};
30+
31+
const {useLayoutEffect, useRef} = React;
32+
33+
const FocusManager = React.forwardRef(
34+
(
35+
{
36+
autoFocus,
37+
children,
38+
containFocus,
39+
restoreFocus,
40+
scope: CustomScope,
41+
}: TabFocusProps,
42+
ref,
43+
): React.Node => {
44+
const ScopeToUse = CustomScope || TabbableScope;
45+
const scopeRef = useRef(null);
46+
// This ensures tabbing works through the React tree (including Portals and Suspense nodes)
47+
const keyboard = useKeyboard({
48+
onKeyDown(event: KeyboardEvent): void {
49+
if (event.key !== 'Tab') {
50+
event.continuePropagation();
51+
return;
52+
}
53+
const scope = scopeRef.current;
54+
if (scope !== null) {
55+
if (event.shiftKey) {
56+
focusPrevious(scope, event, containFocus);
57+
} else {
58+
focusNext(scope, event, containFocus);
59+
}
60+
}
61+
},
62+
});
63+
const focusWithin = useFocusWithin({
64+
onBlurWithin: function(event) {
65+
if (!containFocus) {
66+
event.continuePropagation();
67+
}
68+
const lastNode = event.target;
69+
if (lastNode) {
70+
requestAnimationFrame(() => {
71+
(lastNode: any).focus();
72+
});
73+
}
74+
},
75+
});
76+
useLayoutEffect(
77+
() => {
78+
const scope = scopeRef.current;
79+
let restoreElem;
80+
if (restoreFocus) {
81+
restoreElem = document.activeElement;
82+
}
83+
if (autoFocus && scope !== null) {
84+
focusFirst(scope);
85+
}
86+
if (restoreElem) {
87+
return () => {
88+
(restoreElem: any).focus();
89+
};
90+
}
91+
},
92+
[scopeRef],
93+
);
94+
95+
return (
96+
<ScopeToUse
97+
ref={node => {
98+
if (ref) {
99+
if (typeof ref === 'function') {
100+
ref(node);
101+
} else {
102+
ref.current = node;
103+
}
104+
}
105+
scopeRef.current = node;
106+
}}
107+
listeners={[keyboard, focusWithin]}>
108+
{children}
109+
</ScopeToUse>
110+
);
111+
},
112+
);
113+
114+
export default FocusManager;

packages/react-interactions/accessibility/src/TabFocus.js

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)