From ebc299fc2f21a74fffa3f8aa2209259d98702156 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 24 Sep 2019 23:26:20 +0200 Subject: [PATCH] [react-interactions] TabFocus -> FocusManager (#16874) --- .../{tab-focus.js => focus-manager.js} | 2 +- .../accessibility/src/FocusManager.js | 114 +++++++++++++++++ .../accessibility/src/TabFocus.js | 67 ---------- ...ernal.js => FocusManager-test.internal.js} | 118 ++++++++++++------ .../events/src/dom/Focus.js | 46 ++++++- scripts/rollup/bundles.js | 6 +- 6 files changed, 242 insertions(+), 111 deletions(-) rename packages/react-interactions/accessibility/{tab-focus.js => focus-manager.js} (81%) create mode 100644 packages/react-interactions/accessibility/src/FocusManager.js delete mode 100644 packages/react-interactions/accessibility/src/TabFocus.js rename packages/react-interactions/accessibility/src/__tests__/{TabFocus-test.internal.js => FocusManager-test.internal.js} (75%) diff --git a/packages/react-interactions/accessibility/tab-focus.js b/packages/react-interactions/accessibility/focus-manager.js similarity index 81% rename from packages/react-interactions/accessibility/tab-focus.js rename to packages/react-interactions/accessibility/focus-manager.js index 493333ccad3f..56d396157c22 100644 --- a/packages/react-interactions/accessibility/tab-focus.js +++ b/packages/react-interactions/accessibility/focus-manager.js @@ -9,4 +9,4 @@ 'use strict'; -module.exports = require('./src/TabFocus'); +module.exports = require('./src/FocusManager'); diff --git a/packages/react-interactions/accessibility/src/FocusManager.js b/packages/react-interactions/accessibility/src/FocusManager.js new file mode 100644 index 000000000000..34d4b9e44cfc --- /dev/null +++ b/packages/react-interactions/accessibility/src/FocusManager.js @@ -0,0 +1,114 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactScope} from 'shared/ReactTypes'; +import type {KeyboardEvent} from 'react-interactions/events/keyboard'; + +import React from 'react'; +import {useKeyboard} from 'react-interactions/events/keyboard'; +import {useFocusWithin} from 'react-interactions/events/focus'; +import { + focusFirst, + focusPrevious, + focusNext, +} from 'react-interactions/accessibility/focus-control'; +import TabbableScope from 'react-interactions/accessibility/tabbable-scope'; + +type TabFocusProps = {| + autoFocus?: boolean, + children: React.Node, + containFocus?: boolean, + restoreFocus?: boolean, + scope: ReactScope, +|}; + +const {useLayoutEffect, useRef} = React; + +const FocusManager = React.forwardRef( + ( + { + autoFocus, + children, + containFocus, + restoreFocus, + scope: CustomScope, + }: TabFocusProps, + ref, + ): React.Node => { + const ScopeToUse = CustomScope || TabbableScope; + const scopeRef = useRef(null); + // This ensures tabbing works through the React tree (including Portals and Suspense nodes) + const keyboard = useKeyboard({ + onKeyDown(event: KeyboardEvent): void { + if (event.key !== 'Tab') { + event.continuePropagation(); + return; + } + const scope = scopeRef.current; + if (scope !== null) { + if (event.shiftKey) { + focusPrevious(scope, event, containFocus); + } else { + focusNext(scope, event, containFocus); + } + } + }, + }); + const focusWithin = useFocusWithin({ + onBlurWithin: function(event) { + if (!containFocus) { + event.continuePropagation(); + } + const lastNode = event.target; + if (lastNode) { + requestAnimationFrame(() => { + (lastNode: any).focus(); + }); + } + }, + }); + useLayoutEffect( + () => { + const scope = scopeRef.current; + let restoreElem; + if (restoreFocus) { + restoreElem = document.activeElement; + } + if (autoFocus && scope !== null) { + focusFirst(scope); + } + if (restoreElem) { + return () => { + (restoreElem: any).focus(); + }; + } + }, + [scopeRef], + ); + + return ( + { + if (ref) { + if (typeof ref === 'function') { + ref(node); + } else { + ref.current = node; + } + } + scopeRef.current = node; + }} + listeners={[keyboard, focusWithin]}> + {children} + + ); + }, +); + +export default FocusManager; diff --git a/packages/react-interactions/accessibility/src/TabFocus.js b/packages/react-interactions/accessibility/src/TabFocus.js deleted file mode 100644 index 76e528116cd2..000000000000 --- a/packages/react-interactions/accessibility/src/TabFocus.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {ReactScope} from 'shared/ReactTypes'; -import type {KeyboardEvent} from 'react-interactions/events/keyboard'; - -import React from 'react'; -import {useKeyboard} from 'react-interactions/events/keyboard'; -import { - focusPrevious, - focusNext, -} from 'react-interactions/accessibility/focus-control'; - -type TabFocusProps = { - children: React.Node, - contain?: boolean, - scope: ReactScope, -}; - -const {useRef} = React; - -const TabFocus = React.forwardRef( - ({children, contain, scope: Scope}: TabFocusProps, ref): React.Node => { - const scopeRef = useRef(null); - const keyboard = useKeyboard({ - onKeyDown(event: KeyboardEvent): void { - if (event.key !== 'Tab') { - event.continuePropagation(); - return; - } - const scope = scopeRef.current; - if (scope !== null) { - if (event.shiftKey) { - focusPrevious(scope, event, contain); - } else { - focusNext(scope, event, contain); - } - } - }, - }); - - return ( - { - if (ref) { - if (typeof ref === 'function') { - ref(node); - } else { - ref.current = node; - } - } - scopeRef.current = node; - }} - listeners={keyboard}> - {children} - - ); - }, -); - -export default TabFocus; diff --git a/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/FocusManager-test.internal.js similarity index 75% rename from packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js rename to packages/react-interactions/accessibility/src/__tests__/FocusManager-test.internal.js index 92a2833b9c43..1cf07104c0f3 100644 --- a/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/FocusManager-test.internal.js @@ -11,18 +11,16 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra let React; let ReactFeatureFlags; -let TabFocus; -let TabbableScope; +let FocusManager; let FocusControl; -describe('TabFocusController', () => { +describe('FocusManager', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableScopeAPI = true; ReactFeatureFlags.enableFlareAPI = true; - TabFocus = require('../TabFocus').default; - TabbableScope = require('../TabbableScope').default; + FocusManager = require('../FocusManager').default; FocusControl = require('../FocusControl'); React = require('react'); }); @@ -42,21 +40,21 @@ describe('TabFocusController', () => { container = null; }); - it('handles tab operations', () => { + it('handles tab operations by default', () => { const inputRef = React.createRef(); const input2Ref = React.createRef(); const buttonRef = React.createRef(); - const butto2nRef = React.createRef(); + const button2Ref = React.createRef(); const divRef = React.createRef(); const Test = () => ( - +