From 1a6294d3e2254f03267da3412becbe51188e830f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 23 Sep 2019 21:30:49 +0200 Subject: [PATCH] [react-interaction] Refactor a11y components more (#16866) --- .../accessibility/focus-control.js | 12 ++ .../accessibility/src/FocusControl.js | 139 +++++++++++++++ .../accessibility/src/FocusTable.js | 7 +- .../accessibility/src/TabFocus.js | 160 ++---------------- .../accessibility/src/TabbableScope.js | 4 +- .../src/__tests__/FocusTable-test.internal.js | 6 +- .../src/__tests__/TabFocus-test.internal.js | 60 +++---- .../__tests__/TabbableScope-test.internal.js | 2 +- scripts/rollup/bundles.js | 10 ++ 9 files changed, 220 insertions(+), 180 deletions(-) create mode 100644 packages/react-interactions/accessibility/focus-control.js create mode 100644 packages/react-interactions/accessibility/src/FocusControl.js diff --git a/packages/react-interactions/accessibility/focus-control.js b/packages/react-interactions/accessibility/focus-control.js new file mode 100644 index 0000000000000..ec6366b6b4a49 --- /dev/null +++ b/packages/react-interactions/accessibility/focus-control.js @@ -0,0 +1,12 @@ +/** + * 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 + */ + +'use strict'; + +module.exports = require('./src/FocusControl'); diff --git a/packages/react-interactions/accessibility/src/FocusControl.js b/packages/react-interactions/accessibility/src/FocusControl.js new file mode 100644 index 0000000000000..f3fcabf7d027c --- /dev/null +++ b/packages/react-interactions/accessibility/src/FocusControl.js @@ -0,0 +1,139 @@ +/** + * 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 type {KeyboardEvent} from 'react-interactions/events/keyboard'; + +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, + event?: KeyboardEvent, + contain?: boolean, +): void { + const [ + tabbableNodes, + firstTabbableElem, + lastTabbableElem, + currentIndex, + focusedElement, + ] = getTabbableNodes(scope); + + if (focusedElement === null) { + if (event) { + event.continuePropagation(); + } + } else if (focusedElement === lastTabbableElem) { + if (contain) { + focusElem(firstTabbableElem); + if (event) { + event.preventDefault(); + } + } else if (event) { + event.continuePropagation(); + } + } else { + focusElem((tabbableNodes: any)[currentIndex + 1]); + if (event) { + event.preventDefault(); + } + } +} + +export function focusPrevious( + scope: ReactScopeMethods, + event?: KeyboardEvent, + contain?: boolean, +): void { + const [ + tabbableNodes, + firstTabbableElem, + lastTabbableElem, + currentIndex, + focusedElement, + ] = getTabbableNodes(scope); + + if (focusedElement === null) { + if (event) { + event.continuePropagation(); + } + } else if (focusedElement === firstTabbableElem) { + if (contain) { + focusElem(lastTabbableElem); + if (event) { + event.preventDefault(); + } + } else if (event) { + event.continuePropagation(); + } + } else { + focusElem((tabbableNodes: any)[currentIndex - 1]); + if (event) { + event.preventDefault(); + } + } +} + +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]; +} diff --git a/packages/react-interactions/accessibility/src/FocusTable.js b/packages/react-interactions/accessibility/src/FocusTable.js index 64042bbbec1c5..d405c0f929380 100644 --- a/packages/react-interactions/accessibility/src/FocusTable.js +++ b/packages/react-interactions/accessibility/src/FocusTable.js @@ -11,7 +11,6 @@ import type {ReactScopeMethods} from 'shared/ReactTypes'; import type {KeyboardEvent} from 'react-interactions/events/keyboard'; import React from 'react'; -import {tabFocusableImpl} from 'react-interactions/accessibility/tabbable-scope'; import {useKeyboard} from 'react-interactions/events/keyboard'; type FocusCellProps = { @@ -128,8 +127,10 @@ function triggerNavigateOut( } } -export function createFocusTable(): Array { - const TableScope = React.unstable_createScope(tabFocusableImpl); +export function createFocusTable( + scopeImpl: (type: string, props: Object) => boolean, +): Array { + const TableScope = React.unstable_createScope(scopeImpl); function Table({children, onKeyboardOut, id}): FocusTableProps { return ( diff --git a/packages/react-interactions/accessibility/src/TabFocus.js b/packages/react-interactions/accessibility/src/TabFocus.js index 98ab4c812f07a..76e528116cd2e 100644 --- a/packages/react-interactions/accessibility/src/TabFocus.js +++ b/packages/react-interactions/accessibility/src/TabFocus.js @@ -7,158 +7,26 @@ * @flow */ -import type {ReactScopeMethods} from 'shared/ReactTypes'; +import type {ReactScope} from 'shared/ReactTypes'; import type {KeyboardEvent} from 'react-interactions/events/keyboard'; import React from 'react'; -import {TabbableScope} from 'react-interactions/accessibility/tabbable-scope'; import {useKeyboard} from 'react-interactions/events/keyboard'; +import { + focusPrevious, + focusNext, +} from 'react-interactions/accessibility/focus-control'; -type TabFocusControllerProps = { +type TabFocusProps = { children: React.Node, contain?: boolean, + scope: ReactScope, }; 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(); - } -} - -function internalFocusNext( - scope: ReactScopeMethods, - event?: KeyboardEvent, - contain?: boolean, -): void { - const [ - tabbableNodes, - firstTabbableElem, - lastTabbableElem, - currentIndex, - focusedElement, - ] = getTabbableNodes(scope); - - if (focusedElement === null) { - if (event) { - event.continuePropagation(); - } - } else if (focusedElement === lastTabbableElem) { - if (contain) { - focusElem(firstTabbableElem); - if (event) { - event.preventDefault(); - } - } else if (event) { - event.continuePropagation(); - } - } else { - focusElem((tabbableNodes: any)[currentIndex + 1]); - if (event) { - event.preventDefault(); - } - } -} - -function internalFocusPrevious( - scope: ReactScopeMethods, - event?: KeyboardEvent, - contain?: boolean, -): void { - const [ - tabbableNodes, - firstTabbableElem, - lastTabbableElem, - currentIndex, - focusedElement, - ] = getTabbableNodes(scope); - - if (focusedElement === null) { - if (event) { - event.continuePropagation(); - } - } else if (focusedElement === firstTabbableElem) { - if (contain) { - focusElem(lastTabbableElem); - if (event) { - event.preventDefault(); - } - } else if (event) { - event.continuePropagation(); - } - } else { - focusElem((tabbableNodes: any)[currentIndex - 1]); - if (event) { - event.preventDefault(); - } - } -} - -export function focusPrevious(scope: ReactScopeMethods): void { - internalFocusPrevious(scope); -} - -export function focusNext(scope: ReactScopeMethods): void { - internalFocusNext(scope); -} - -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 TabFocus = React.forwardRef( + ({children, contain, scope: Scope}: TabFocusProps, ref): React.Node => { const scopeRef = useRef(null); const keyboard = useKeyboard({ onKeyDown(event: KeyboardEvent): void { @@ -169,16 +37,16 @@ export const TabFocusController = React.forwardRef( const scope = scopeRef.current; if (scope !== null) { if (event.shiftKey) { - internalFocusPrevious(scope, event, contain); + focusPrevious(scope, event, contain); } else { - internalFocusNext(scope, event, contain); + focusNext(scope, event, contain); } } }, }); return ( - { if (ref) { if (typeof ref === 'function') { @@ -191,7 +59,9 @@ export const TabFocusController = React.forwardRef( }} listeners={keyboard}> {children} - + ); }, ); + +export default TabFocus; diff --git a/packages/react-interactions/accessibility/src/TabbableScope.js b/packages/react-interactions/accessibility/src/TabbableScope.js index 6a22a55b29883..7c025f63afeb9 100644 --- a/packages/react-interactions/accessibility/src/TabbableScope.js +++ b/packages/react-interactions/accessibility/src/TabbableScope.js @@ -32,4 +32,6 @@ export const tabFocusableImpl = (type: string, props: Object): boolean => { ); }; -export const TabbableScope = React.unstable_createScope(tabFocusableImpl); +const TabbableScope = React.unstable_createScope(tabFocusableImpl); + +export default TabbableScope; diff --git a/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js index 83f6ee3097ddd..d05bec5ba8fb5 100644 --- a/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js @@ -12,6 +12,7 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra let React; let ReactFeatureFlags; let createFocusTable; +let tabFocusableImpl; describe('ReactFocusTable', () => { beforeEach(() => { @@ -20,6 +21,7 @@ describe('ReactFocusTable', () => { ReactFeatureFlags.enableScopeAPI = true; ReactFeatureFlags.enableFlareAPI = true; createFocusTable = require('../FocusTable').createFocusTable; + tabFocusableImpl = require('../TabbableScope').tabFocusableImpl; React = require('react'); }); @@ -39,7 +41,9 @@ describe('ReactFocusTable', () => { }); function createFocusTableComponent() { - const [FocusTable, FocusTableRow, FocusTableCell] = createFocusTable(); + const [FocusTable, FocusTableRow, FocusTableCell] = createFocusTable( + tabFocusableImpl, + ); return ({onKeyboardOut, id}) => ( diff --git a/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js b/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js index 06f7aebae80ac..92a2833b9c43c 100644 --- a/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js +++ b/packages/react-interactions/accessibility/src/__tests__/TabFocus-test.internal.js @@ -11,8 +11,9 @@ import {createEventTarget} from 'react-interactions/events/src/dom/testing-libra let React; let ReactFeatureFlags; -let TabFocusController; -let ReactTabFocus; +let TabFocus; +let TabbableScope; +let FocusControl; describe('TabFocusController', () => { beforeEach(() => { @@ -20,8 +21,9 @@ describe('TabFocusController', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableScopeAPI = true; ReactFeatureFlags.enableFlareAPI = true; - ReactTabFocus = require('../TabFocus'); - TabFocusController = ReactTabFocus.TabFocusController; + TabFocus = require('../TabFocus').default; + TabbableScope = require('../TabbableScope').default; + FocusControl = require('../FocusControl'); React = require('react'); }); @@ -48,13 +50,13 @@ describe('TabFocusController', () => { const divRef = React.createRef(); const Test = () => ( - +