@samitouri / QOS-React-2 / commits / ebc299fc2f

[react-interactions] TabFocus -> FocusManager (#16874)

Dominic Gannaway committed Sep 24, 2019 at 23:26 UTC ebc299fc2f21a74fffa3f8aa2209259d98702156
6 files changed +242 -111
packages/react-interactions/accessibility/focus-manager.js renamed
+1 -1
@@ -9,4 +9,4 @@
9
10 'use strict';
11
12 -module.exports = require('./src/TabFocus');
12 +module.exports = require('./src/FocusManager');
packages/react-interactions/accessibility/src/FocusManager.js new
+114
@@ -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 deleted
-67
@@ -1,67 +0,0 @@
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 {
16 - focusPrevious,
17 - focusNext,
18 -} from 'react-interactions/accessibility/focus-control';
19 -
20 -type TabFocusProps = {
21 - children: React.Node,
22 - contain?: boolean,
23 - scope: ReactScope,
24 -};
25 -
26 -const {useRef} = React;
27 -
28 -const TabFocus = React.forwardRef(
29 - ({children, contain, scope: Scope}: TabFocusProps, ref): React.Node => {
30 - const scopeRef = useRef(null);
31 - const keyboard = useKeyboard({
32 - onKeyDown(event: KeyboardEvent): void {
33 - if (event.key !== 'Tab') {
34 - event.continuePropagation();
35 - return;
36 - }
37 - const scope = scopeRef.current;
38 - if (scope !== null) {
39 - if (event.shiftKey) {
40 - focusPrevious(scope, event, contain);
41 - } else {
42 - focusNext(scope, event, contain);
43 - }
44 - }
45 - },
46 - });
47 -
48 - return (
49 - <Scope
50 - ref={node => {
51 - if (ref) {
52 - if (typeof ref === 'function') {
53 - ref(node);
54 - } else {
55 - ref.current = node;
56 - }
57 - }
58 - scopeRef.current = node;
59 - }}
60 - listeners={keyboard}>
61 - {children}
62 - </Scope>
63 - );
64 - },
65 -);
66 -
67 -export default TabFocus;
packages/react-interactions/accessibility/src/__tests__/FocusManager-test.internal.js renamed
+83 -35
@@ -11,18 +11,16 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra
11
12 let React;
13 let ReactFeatureFlags;
14 -let TabFocus;
15 -let TabbableScope;
14 +let FocusManager;
15 let FocusControl;
16
18 -describe('TabFocusController', () => {
17 +describe('FocusManager', () => {
18 beforeEach(() => {
19 jest.resetModules();
20 ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 ReactFeatureFlags.enableScopeAPI = true;
22 ReactFeatureFlags.enableFlareAPI = true;
24 - TabFocus = require('../TabFocus').default;
25 - TabbableScope = require('../TabbableScope').default;
23 + FocusManager = require('../FocusManager').default;
24 FocusControl = require('../FocusControl');
25 React = require('react');
26 });
@@ -42,21 +40,21 @@ describe('TabFocusController', () => {
40 container = null;
41 });
42
45 - it('handles tab operations', () => {
43 + it('handles tab operations by default', () => {
44 const inputRef = React.createRef();
45 const input2Ref = React.createRef();
46 const buttonRef = React.createRef();
49 - const butto2nRef = React.createRef();
47 + const button2Ref = React.createRef();
48 const divRef = React.createRef();
49
50 const Test = () => (
53 - <TabFocus scope={TabbableScope}>
51 + <FocusManager>
52 <input ref={inputRef} />
53 <button ref={buttonRef} />
54 <div ref={divRef} tabIndex={0} />
55 <input ref={input2Ref} tabIndex={-1} />
58 - <button ref={butto2nRef} />
59 - </TabFocus>
56 + <button ref={button2Ref} />
57 + </FocusManager>
58 );
59
60 ReactDOM.render(<Test />, container);
@@ -66,24 +64,67 @@ describe('TabFocusController', () => {
64 createEventTarget(document.activeElement).tabNext();
65 expect(document.activeElement).toBe(divRef.current);
66 createEventTarget(document.activeElement).tabNext();
69 - expect(document.activeElement).toBe(butto2nRef.current);
67 + expect(document.activeElement).toBe(button2Ref.current);
68 createEventTarget(document.activeElement).tabPrevious();
69 expect(document.activeElement).toBe(divRef.current);
70 });
71
74 - it('handles tab operations with containment', () => {
72 + it('handles autoFocus', () => {
73 + const buttonRef = React.createRef();
74 +
75 + const Test = () => (
76 + <FocusManager autoFocus={true}>
77 + <input tabIndex={-1} />
78 + <button ref={buttonRef} />
79 + </FocusManager>
80 + );
81 +
82 + ReactDOM.render(<Test />, container);
83 + expect(document.activeElement).toBe(buttonRef.current);
84 + });
85 +
86 + it('handles restoreFocus', () => {
87 + const difRef = React.createRef();
88 + const buttonRef = React.createRef();
89 +
90 + const Test = ({flag}) => {
91 + return (
92 + <div ref={difRef} tabIndex={0}>
93 + {flag ? (
94 + <FocusManager autoFocus={true} restoreFocus={true}>
95 + <button ref={buttonRef} />
96 + </FocusManager>
97 + ) : null}
98 + </div>
99 + );
100 + };
101 +
102 + ReactDOM.render(<Test flag={false} />, container);
103 + difRef.current.focus();
104 + expect(document.activeElement).toBe(difRef.current);
105 + ReactDOM.render(<Test flag={true} />, container);
106 + expect(document.activeElement).toBe(buttonRef.current);
107 + ReactDOM.render(<Test flag={false} />, container);
108 + expect(document.activeElement).toBe(difRef.current);
109 + });
110 +
111 + it('handles containFocus', () => {
112 const inputRef = React.createRef();
113 const input2Ref = React.createRef();
114 + const input3Ref = React.createRef();
115 const buttonRef = React.createRef();
116 const button2Ref = React.createRef();
117
118 const Test = () => (
81 - <TabFocus scope={TabbableScope} contain={true}>
82 - <input ref={inputRef} tabIndex={-1} />
83 - <button ref={buttonRef} id={1} />
84 - <button ref={button2Ref} id={2} />
85 - <input ref={input2Ref} tabIndex={-1} />
86 - </TabFocus>
119 + <div>
120 + <FocusManager containFocus={true}>
121 + <input ref={inputRef} tabIndex={-1} />
122 + <button ref={buttonRef} id={1} />
123 + <button ref={button2Ref} id={2} />
124 + <input ref={input2Ref} tabIndex={-1} />
125 + </FocusManager>
126 + <input ref={input3Ref} />
127 + </div>
128 );
129
130 ReactDOM.render(<Test />, container);
@@ -98,9 +139,16 @@ describe('TabFocusController', () => {
139 expect(document.activeElement).toBe(buttonRef.current);
140 createEventTarget(document.activeElement).tabPrevious();
141 expect(document.activeElement).toBe(button2Ref.current);
142 + // Focus should be restored to the contained area
143 + const rAF = window.requestAnimationFrame;
144 + window.requestAnimationFrame = x => setTimeout(x);
145 + input3Ref.current.focus();
146 + jest.advanceTimersByTime(1);
147 + window.requestAnimationFrame = rAF;
148 + expect(document.activeElement).toBe(button2Ref.current);
149 });
150
103 - it('handles tab operations when controllers are nested', () => {
151 + it('works with nested FocusManagers', () => {
152 const inputRef = React.createRef();
153 const input2Ref = React.createRef();
154 const buttonRef = React.createRef();
@@ -109,16 +157,16 @@ describe('TabFocusController', () => {
157 const button4Ref = React.createRef();
158
159 const Test = () => (
112 - <TabFocus scope={TabbableScope}>
160 + <FocusManager>
161 <input ref={inputRef} tabIndex={-1} />
162 <button ref={buttonRef} id={1} />
115 - <TabFocus scope={TabbableScope}>
163 + <FocusManager>
164 <button ref={button2Ref} id={2} />
165 <button ref={button3Ref} id={3} />
118 - </TabFocus>
166 + </FocusManager>
167 <input ref={input2Ref} tabIndex={-1} />
168 <button ref={button4Ref} id={4} />
121 - </TabFocus>
169 + </FocusManager>
170 );
171
172 ReactDOM.render(<Test />, container);
@@ -136,7 +184,7 @@ describe('TabFocusController', () => {
184 expect(document.activeElement).toBe(button2Ref.current);
185 });
186
139 - it('handles tab operations when controllers are nested with containment', () => {
187 + it('handles containFocus (nested FocusManagers)', () => {
188 const inputRef = React.createRef();
189 const input2Ref = React.createRef();
190 const buttonRef = React.createRef();
@@ -145,16 +193,16 @@ describe('TabFocusController', () => {
193 const button4Ref = React.createRef();
194
195 const Test = () => (
148 - <TabFocus scope={TabbableScope}>
196 + <FocusManager>
197 <input ref={inputRef} tabIndex={-1} />
198 <button ref={buttonRef} id={1} />
151 - <TabFocus contain={true} scope={TabbableScope}>
199 + <FocusManager containFocus={true}>
200 <button ref={button2Ref} id={2} />
201 <button ref={button3Ref} id={3} />
154 - </TabFocus>
202 + </FocusManager>
203 <input ref={input2Ref} tabIndex={-1} />
204 <button ref={button4Ref} id={4} />
157 - </TabFocus>
205 + </FocusManager>
206 );
207
208 ReactDOM.render(<Test />, container);
@@ -195,14 +243,14 @@ describe('TabFocusController', () => {
243 }
244
245 const Test = () => (
198 - <TabFocus scope={TabbableScope}>
246 + <FocusManager>
247 <button ref={buttonRef} id={1} />
248 <button ref={button2Ref} id={2} />
249 <React.Suspense fallback={<button ref={button3Ref} id={3} />}>
250 <Component />
251 </React.Suspense>
252 <button ref={button4Ref} id={4} />
205 - </TabFocus>
253 + </FocusManager>
254 );
255
256 ReactDOM.render(<Test />, container);
@@ -220,7 +268,7 @@ describe('TabFocusController', () => {
268 expect(document.activeElement).toBe(button2Ref.current);
269 });
270
223 - it('allows for imperative tab focus control', () => {
271 + it('allows for imperative tab focus control using FocusControl', () => {
272 const firstFocusControllerRef = React.createRef();
273 const secondFocusControllerRef = React.createRef();
274 const buttonRef = React.createRef();
@@ -229,16 +277,16 @@ describe('TabFocusController', () => {
277
278 const Test = () => (
279 <div>
232 - <TabFocus ref={firstFocusControllerRef} scope={TabbableScope}>
280 + <FocusManager ref={firstFocusControllerRef}>
281 <input tabIndex={-1} />
282 <button ref={buttonRef} />
283 <button ref={button2Ref} />
284 <input tabIndex={-1} />
237 - </TabFocus>
238 - <TabFocus ref={secondFocusControllerRef} scope={TabbableScope}>
285 + </FocusManager>
286 + <FocusManager ref={secondFocusControllerRef}>
287 <input tabIndex={-1} />
288 <div ref={divRef} tabIndex={0} />
241 - </TabFocus>
289 + </FocusManager>
290 </div>
291 );
292
packages/react-interactions/events/src/dom/Focus.js
+40 -6
@@ -26,6 +26,7 @@ type FocusEvent = {|
26 type: FocusEventType | FocusWithinEventType,
27 pointerType: PointerType,
28 timeStamp: number,
29 + continuePropagation: () => void,
30 |};
31
32 type FocusState = {
@@ -47,12 +48,16 @@ type FocusProps = {
48 type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
49
50 type FocusWithinProps = {
50 - disabled: boolean,
51 - onFocusWithinChange: boolean => void,
52 - onFocusWithinVisibleChange: boolean => void,
51 + disabled?: boolean,
52 + onBlurWithin?: (e: FocusEvent) => void,
53 + onFocusWithinChange?: boolean => void,
54 + onFocusWithinVisibleChange?: boolean => void,
55 };
56
55 -type FocusWithinEventType = 'focuswithinvisiblechange' | 'focuswithinchange';
57 +type FocusWithinEventType =
58 + | 'focuswithinvisiblechange'
59 + | 'focuswithinchange'
60 + | 'blurwithin';
61
62 /**
63 * Shared between Focus and FocusWithin
@@ -89,6 +94,14 @@ function createFocusEvent(
94 type,
95 pointerType,
96 timeStamp: context.getTimeStamp(),
97 + // We don't use stopPropagation, as the default behavior
98 + // is to not propagate. Plus, there might be confusion
99 + // using stopPropagation as we don't actually stop
100 + // native propagation from working, but instead only
101 + // allow propagation to the others keyboard responders.
102 + continuePropagation() {
103 + context.continuePropagation();
104 + },
105 };
106 }
107
@@ -226,6 +239,26 @@ function dispatchBlurEvents(
239 }
240 }
241
242 +function dispatchBlurWithinEvents(
243 + context: ReactDOMResponderContext,
244 + event: ReactDOMResponderEvent,
245 + props: FocusWithinProps,
246 + state: FocusState,
247 +) {
248 + const pointerType = state.pointerType;
249 + const target = ((state.focusTarget: any): Element | Document) || event.target;
250 + const onBlurWithin = (props.onBlurWithin: any);
251 + if (isFunction(onBlurWithin)) {
252 + const syntheticEvent = createFocusEvent(
253 + context,
254 + 'blurwithin',
255 + target,
256 + pointerType,
257 + );
258 + context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent);
259 + }
260 +}
261 +
262 function dispatchFocusChange(
263 context: ReactDOMResponderContext,
264 props: FocusProps,
@@ -364,7 +397,7 @@ function dispatchFocusWithinChangeEvent(
397 state: FocusState,
398 value: boolean,
399 ) {
367 - const onFocusWithinChange = props.onFocusWithinChange;
400 + const onFocusWithinChange = (props.onFocusWithinChange: any);
401 if (isFunction(onFocusWithinChange)) {
402 context.dispatchEvent(value, onFocusWithinChange, DiscreteEvent);
403 }
@@ -379,7 +412,7 @@ function dispatchFocusWithinVisibleChangeEvent(
412 state: FocusState,
413 value: boolean,
414 ) {
382 - const onFocusWithinVisibleChange = props.onFocusWithinVisibleChange;
415 + const onFocusWithinVisibleChange = (props.onFocusWithinVisibleChange: any);
416 if (isFunction(onFocusWithinVisibleChange)) {
417 context.dispatchEvent(value, onFocusWithinVisibleChange, DiscreteEvent);
418 }
@@ -447,6 +480,7 @@ const focusWithinResponderImpl = {
480 !context.isTargetWithinResponder(relatedTarget)
481 ) {
482 dispatchFocusWithinChangeEvent(context, props, state, false);
483 + dispatchBlurWithinEvents(context, event, props, state);
484 state.isFocused = false;
485 }
486 break;
scripts/rollup/bundles.js
+4 -2
@@ -681,11 +681,12 @@ const bundles = [
681 {
682 bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
683 moduleType: NON_FIBER_RENDERER,
684 - entry: 'react-interactions/accessibility/tab-focus',
685 - global: 'ReactTabFocus',
684 + entry: 'react-interactions/accessibility/focus-manager',
685 + global: 'ReactFocusManager',
686 externals: [
687 'react',
688 'react-interactions/events/keyboard',
689 + 'react-interactions/events/focus',
690 'react-interactions/accessibility/tabbable-scope',
691 'react-interactions/accessibility/focus-control',
692 ],
@@ -721,6 +722,7 @@ const bundles = [
722 ];
723
724 const fbBundleExternalsMap = {
725 + 'react-interactions/events/focus': 'ReactEventsFocus',
726 'react-interactions/events/keyboard': 'ReactEventsKeyboard',
727 'react-interactions/events/tap': 'ReactEventsTap',
728 'react-interactions/accessibility/tabbable-scope': 'ReactTabbableScope',