From 4b0b556dcf6f84c3e4a4e2a9dd0426c3430d1c86 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 12 Sep 2019 22:28:07 +0200 Subject: [PATCH] [react-interactions] Refactor TabFocusController (#16768) --- .../src/client/focus/ReactTabFocus.js | 188 ++++++++++++++++++ .../src/client/focus/TabFocusController.js | 179 ----------------- .../TabFocusController-test.internal.js | 22 +- .../react-reconciler/src/ReactFiberScope.js | 4 - packages/shared/ReactTypes.js | 1 - 5 files changed, 202 insertions(+), 192 deletions(-) create mode 100644 packages/react-dom/src/client/focus/ReactTabFocus.js delete mode 100644 packages/react-dom/src/client/focus/TabFocusController.js diff --git a/packages/react-dom/src/client/focus/ReactTabFocus.js b/packages/react-dom/src/client/focus/ReactTabFocus.js new file mode 100644 index 000000000000..684015526116 --- /dev/null +++ b/packages/react-dom/src/client/focus/ReactTabFocus.js @@ -0,0 +1,188 @@ +/** + * 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 {ReactScopeMethods} from 'shared/ReactTypes'; + +import React from 'react'; +import {TabbableScope} from './TabbableScope'; +import {useKeyboard} from 'react-events/keyboard'; + +type TabFocusControllerProps = { + children: React.Node, + contain?: boolean, +}; + +type KeyboardEventType = 'keydown' | 'keyup'; + +type KeyboardEvent = {| + altKey: boolean, + ctrlKey: boolean, + isComposing: boolean, + key: string, + metaKey: boolean, + shiftKey: boolean, + target: Element | Document, + type: KeyboardEventType, + timeStamp: number, + defaultPrevented: boolean, +|}; + +const {useRef} = React; + +function getTabbableNodes(scope: ReactScopeMethods) { + const tabbableNodes = scope.getScopedNodes(); + if (tabbableNodes === null || tabbableNodes.length === 0) { + return [null, null, null, 0, null]; + } + const firstTabbableElem = tabbableNodes[0]; + const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1]; + const currentIndex = tabbableNodes.indexOf(document.activeElement); + let focusedElement = null; + if (currentIndex !== -1) { + focusedElement = tabbableNodes[currentIndex]; + } + return [ + tabbableNodes, + firstTabbableElem, + lastTabbableElem, + currentIndex, + focusedElement, + ]; +} + +export function focusFirst(scope: ReactScopeMethods): void { + const [, firstTabbableElem] = getTabbableNodes(scope); + focusElem(firstTabbableElem); +} + +function focusElem(elem: null | HTMLElement): void { + if (elem !== null) { + elem.focus(); + } +} + +export function focusNext( + scope: ReactScopeMethods, + contain?: boolean, +): boolean { + const [ + tabbableNodes, + firstTabbableElem, + lastTabbableElem, + currentIndex, + focusedElement, + ] = getTabbableNodes(scope); + + if (focusedElement === null) { + focusElem(firstTabbableElem); + } else if (focusedElement === lastTabbableElem) { + if (contain === true) { + focusElem(firstTabbableElem); + } else { + return true; + } + } else { + focusElem((tabbableNodes: any)[currentIndex + 1]); + } + return false; +} + +export function focusPrevious( + scope: ReactScopeMethods, + contain?: boolean, +): boolean { + const [ + tabbableNodes, + firstTabbableElem, + lastTabbableElem, + currentIndex, + focusedElement, + ] = getTabbableNodes(scope); + + if (focusedElement === null) { + focusElem(firstTabbableElem); + } else if (focusedElement === firstTabbableElem) { + if (contain === true) { + focusElem(lastTabbableElem); + } else { + return true; + } + } else { + focusElem((tabbableNodes: any)[currentIndex - 1]); + } + return false; +} + +export function getNextController( + scope: ReactScopeMethods, +): null | ReactScopeMethods { + const allScopes = scope.getChildrenFromRoot(); + if (allScopes === null) { + return null; + } + const currentScopeIndex = allScopes.indexOf(scope); + if (currentScopeIndex === -1 || currentScopeIndex === allScopes.length - 1) { + return null; + } + return allScopes[currentScopeIndex + 1]; +} + +export function getPreviousController( + scope: ReactScopeMethods, +): null | ReactScopeMethods { + const allScopes = scope.getChildrenFromRoot(); + if (allScopes === null) { + return null; + } + const currentScopeIndex = allScopes.indexOf(scope); + if (currentScopeIndex <= 0) { + return null; + } + return allScopes[currentScopeIndex - 1]; +} + +export const TabFocusController = React.forwardRef( + ({children, contain}: TabFocusControllerProps, ref): React.Node => { + const scopeRef = useRef(null); + const keyboard = useKeyboard({ + onKeyDown(event: KeyboardEvent): boolean { + if (event.key !== 'Tab') { + return true; + } + const scope = scopeRef.current; + if (scope !== null) { + if (event.shiftKey) { + return focusPrevious(scope, contain); + } else { + return focusNext(scope, contain); + } + } + return true; + }, + preventKeys: ['Tab', ['Tab', {shiftKey: true}]], + }); + + return ( + { + if (ref) { + if (typeof ref === 'function') { + ref(node); + } else { + ref.current = node; + } + } + scopeRef.current = node; + }} + listeners={keyboard}> + {children} + + ); + }, +); diff --git a/packages/react-dom/src/client/focus/TabFocusController.js b/packages/react-dom/src/client/focus/TabFocusController.js deleted file mode 100644 index fbf941fcefbf..000000000000 --- a/packages/react-dom/src/client/focus/TabFocusController.js +++ /dev/null @@ -1,179 +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 React from 'react'; -import {TabbableScope} from './TabbableScope'; -import {useKeyboard} from 'react-events/keyboard'; - -type TabFocusControllerProps = { - children: React.Node, - contain?: boolean, -}; - -type KeyboardEventType = 'keydown' | 'keyup'; - -type KeyboardEvent = {| - altKey: boolean, - ctrlKey: boolean, - isComposing: boolean, - key: string, - metaKey: boolean, - shiftKey: boolean, - target: Element | Document, - type: KeyboardEventType, - timeStamp: number, - defaultPrevented: boolean, -|}; - -type ControllerHandle = {| - focusFirst: () => void, - focusNext: () => boolean, - focusPrevious: () => boolean, - getNextController: () => null | ControllerHandle, - getPreviousController: () => null | ControllerHandle, -|}; - -const {useImperativeHandle, useRef} = React; - -function getTabbableNodes(scopeRef) { - const tabbableScope = scopeRef.current; - const tabbableNodes = tabbableScope.getScopedNodes(); - const firstTabbableElem = tabbableNodes[0]; - const lastTabbableElem = tabbableNodes[tabbableNodes.length - 1]; - const currentIndex = tabbableNodes.indexOf(document.activeElement); - let focusedElement = null; - if (currentIndex !== -1) { - focusedElement = tabbableNodes[currentIndex]; - } - return [ - tabbableNodes, - firstTabbableElem, - lastTabbableElem, - currentIndex, - focusedElement, - ]; -} - -export const TabFocusController = React.forwardRef( - ({children, contain}: TabFocusControllerProps, ref): React.Node => { - const scopeRef = useRef(null); - const keyboard = useKeyboard({ - onKeyDown(event: KeyboardEvent): boolean { - if (event.key !== 'Tab') { - return true; - } - if (event.shiftKey) { - return focusPrevious(); - } else { - return focusNext(); - } - }, - preventKeys: ['Tab', ['Tab', {shiftKey: true}]], - }); - - function focusFirst(): void { - const [, firstTabbableElem] = getTabbableNodes(scopeRef); - firstTabbableElem.focus(); - } - - function focusNext(): boolean { - const [ - tabbableNodes, - firstTabbableElem, - lastTabbableElem, - currentIndex, - focusedElement, - ] = getTabbableNodes(scopeRef); - - if (focusedElement === null) { - firstTabbableElem.focus(); - } else if (focusedElement === lastTabbableElem) { - if (contain === true) { - firstTabbableElem.focus(); - } else { - return true; - } - } else { - tabbableNodes[currentIndex + 1].focus(); - } - return false; - } - - function focusPrevious(): boolean { - const [ - tabbableNodes, - firstTabbableElem, - lastTabbableElem, - currentIndex, - focusedElement, - ] = getTabbableNodes(scopeRef); - - if (focusedElement === null) { - firstTabbableElem.focus(); - } else if (focusedElement === firstTabbableElem) { - if (contain === true) { - lastTabbableElem.focus(); - } else { - return true; - } - } else { - tabbableNodes[currentIndex - 1].focus(); - } - return false; - } - - function getPreviousController(): null | ControllerHandle { - const tabbableScope = scopeRef.current; - const allScopes = tabbableScope.getChildrenFromRoot(); - if (allScopes === null) { - return null; - } - const currentScopeIndex = allScopes.indexOf(tabbableScope); - if (currentScopeIndex <= 0) { - return null; - } - return allScopes[currentScopeIndex - 1].getHandle(); - } - - function getNextController(): null | ControllerHandle { - const tabbableScope = scopeRef.current; - const allScopes = tabbableScope.getChildrenFromRoot(); - if (allScopes === null) { - return null; - } - const currentScopeIndex = allScopes.indexOf(tabbableScope); - if ( - currentScopeIndex === -1 || - currentScopeIndex === allScopes.length - 1 - ) { - return null; - } - return allScopes[currentScopeIndex + 1].getHandle(); - } - - const controllerHandle: ControllerHandle = { - focusFirst, - focusNext, - focusPrevious, - getNextController, - getPreviousController, - }; - - useImperativeHandle(ref, () => controllerHandle); - - return ( - - {children} - - ); - }, -); diff --git a/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js b/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js index 71cc56301f5e..b9846f405412 100644 --- a/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js +++ b/packages/react-dom/src/client/focus/__tests__/TabFocusController-test.internal.js @@ -12,6 +12,7 @@ import {createEventTarget} from 'react-events/src/dom/testing-library'; let React; let ReactFeatureFlags; let TabFocusController; +let ReactTabFocus; describe('TabFocusController', () => { beforeEach(() => { @@ -19,7 +20,8 @@ describe('TabFocusController', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableScopeAPI = true; ReactFeatureFlags.enableFlareAPI = true; - TabFocusController = require('../TabFocusController').TabFocusController; + ReactTabFocus = require('../ReactTabFocus'); + TabFocusController = ReactTabFocus.TabFocusController; React = require('react'); }); @@ -242,21 +244,25 @@ describe('TabFocusController', () => { const firstFocusController = firstFocusControllerRef.current; const secondFocusController = secondFocusControllerRef.current; - firstFocusController.focusFirst(); + ReactTabFocus.focusFirst(firstFocusController); expect(document.activeElement).toBe(buttonRef.current); - firstFocusController.focusNext(); + ReactTabFocus.focusNext(firstFocusController); expect(document.activeElement).toBe(button2Ref.current); - firstFocusController.focusPrevious(); + ReactTabFocus.focusPrevious(firstFocusController); expect(document.activeElement).toBe(buttonRef.current); - const nextController = firstFocusController.getNextController(); + const nextController = ReactTabFocus.getNextController( + firstFocusController, + ); expect(nextController).toBe(secondFocusController); - nextController.focusNext(); + ReactTabFocus.focusNext(nextController); expect(document.activeElement).toBe(divRef.current); - const previousController = nextController.getPreviousController(); + const previousController = ReactTabFocus.getPreviousController( + nextController, + ); expect(previousController).toBe(firstFocusController); - previousController.focusNext(); + ReactTabFocus.focusNext(previousController); expect(document.activeElement).toBe(buttonRef.current); }); }); diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js index bdbae5e697be..64c08df649c7 100644 --- a/packages/react-reconciler/src/ReactFiberScope.js +++ b/packages/react-reconciler/src/ReactFiberScope.js @@ -133,10 +133,6 @@ export function createScopeMethods( collectNearestChildScopeMethods(node.child, scope, childrenScopes); return childrenScopes.length === 0 ? null : childrenScopes; }, - getHandle(): null | mixed { - const currentFiber = ((instance.fiber: any): Fiber); - return currentFiber.memoizedProps.handle || null; - }, getParent(): null | ReactScopeMethods { let node = ((instance.fiber: any): Fiber).return; while (node !== null) { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index ddc713980ca3..cd60a570c64f 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -167,7 +167,6 @@ export type ReactScope = {| export type ReactScopeMethods = {| getChildren(): null | Array, getChildrenFromRoot(): null | Array, - getHandle(): null | mixed, getParent(): null | ReactScopeMethods, getScopedNodes(): null | Array, |};