[react-interaction] Refactor a11y components more (#16866)

This commit is contained in:
Dominic Gannaway
2019-09-23 21:30:49 +02:00
committed by GitHub
parent 1758b3f7ba
commit 1a6294d3e2
9 changed files with 220 additions and 180 deletions
@@ -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');
@@ -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];
}
@@ -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<React.Component> {
const TableScope = React.unstable_createScope(tabFocusableImpl);
export function createFocusTable(
scopeImpl: (type: string, props: Object) => boolean,
): Array<React.Component> {
const TableScope = React.unstable_createScope(scopeImpl);
function Table({children, onKeyboardOut, id}): FocusTableProps {
return (
+15 -145
View File
@@ -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 (
<TabbableScope
<Scope
ref={node => {
if (ref) {
if (typeof ref === 'function') {
@@ -191,7 +59,9 @@ export const TabFocusController = React.forwardRef(
}}
listeners={keyboard}>
{children}
</TabbableScope>
</Scope>
);
},
);
export default TabFocus;
@@ -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;
@@ -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}) => (
<FocusTable onKeyboardOut={onKeyboardOut} id={id}>
@@ -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 = () => (
<TabFocusController>
<TabFocus scope={TabbableScope}>
<input ref={inputRef} />
<button ref={buttonRef} />
<div ref={divRef} tabIndex={0} />
<input ref={input2Ref} tabIndex={-1} />
<button ref={butto2nRef} />
</TabFocusController>
</TabFocus>
);
ReactDOM.render(<Test />, container);
@@ -76,12 +78,12 @@ describe('TabFocusController', () => {
const button2Ref = React.createRef();
const Test = () => (
<TabFocusController contain={true}>
<TabFocus scope={TabbableScope} contain={true}>
<input ref={inputRef} tabIndex={-1} />
<button ref={buttonRef} id={1} />
<button ref={button2Ref} id={2} />
<input ref={input2Ref} tabIndex={-1} />
</TabFocusController>
</TabFocus>
);
ReactDOM.render(<Test />, container);
@@ -107,16 +109,16 @@ describe('TabFocusController', () => {
const button4Ref = React.createRef();
const Test = () => (
<TabFocusController>
<TabFocus scope={TabbableScope}>
<input ref={inputRef} tabIndex={-1} />
<button ref={buttonRef} id={1} />
<TabFocusController>
<TabFocus scope={TabbableScope}>
<button ref={button2Ref} id={2} />
<button ref={button3Ref} id={3} />
</TabFocusController>
</TabFocus>
<input ref={input2Ref} tabIndex={-1} />
<button ref={button4Ref} id={4} />
</TabFocusController>
</TabFocus>
);
ReactDOM.render(<Test />, container);
@@ -143,16 +145,16 @@ describe('TabFocusController', () => {
const button4Ref = React.createRef();
const Test = () => (
<TabFocusController>
<TabFocus scope={TabbableScope}>
<input ref={inputRef} tabIndex={-1} />
<button ref={buttonRef} id={1} />
<TabFocusController contain={true}>
<TabFocus contain={true} scope={TabbableScope}>
<button ref={button2Ref} id={2} />
<button ref={button3Ref} id={3} />
</TabFocusController>
</TabFocus>
<input ref={input2Ref} tabIndex={-1} />
<button ref={button4Ref} id={4} />
</TabFocusController>
</TabFocus>
);
ReactDOM.render(<Test />, container);
@@ -193,14 +195,14 @@ describe('TabFocusController', () => {
}
const Test = () => (
<TabFocusController>
<TabFocus scope={TabbableScope}>
<button ref={buttonRef} id={1} />
<button ref={button2Ref} id={2} />
<React.Suspense fallback={<button ref={button3Ref} id={3} />}>
<Component />
</React.Suspense>
<button ref={button4Ref} id={4} />
</TabFocusController>
</TabFocus>
);
ReactDOM.render(<Test />, container);
@@ -227,16 +229,16 @@ describe('TabFocusController', () => {
const Test = () => (
<div>
<TabFocusController ref={firstFocusControllerRef}>
<TabFocus ref={firstFocusControllerRef} scope={TabbableScope}>
<input tabIndex={-1} />
<button ref={buttonRef} />
<button ref={button2Ref} />
<input tabIndex={-1} />
</TabFocusController>
<TabFocusController ref={secondFocusControllerRef}>
</TabFocus>
<TabFocus ref={secondFocusControllerRef} scope={TabbableScope}>
<input tabIndex={-1} />
<div ref={divRef} tabIndex={0} />
</TabFocusController>
</TabFocus>
</div>
);
@@ -244,25 +246,25 @@ describe('TabFocusController', () => {
const firstFocusController = firstFocusControllerRef.current;
const secondFocusController = secondFocusControllerRef.current;
ReactTabFocus.focusFirst(firstFocusController);
FocusControl.focusFirst(firstFocusController);
expect(document.activeElement).toBe(buttonRef.current);
ReactTabFocus.focusNext(firstFocusController);
FocusControl.focusNext(firstFocusController);
expect(document.activeElement).toBe(button2Ref.current);
ReactTabFocus.focusPrevious(firstFocusController);
FocusControl.focusPrevious(firstFocusController);
expect(document.activeElement).toBe(buttonRef.current);
const nextController = ReactTabFocus.getNextController(
const nextController = FocusControl.getNextController(
firstFocusController,
);
expect(nextController).toBe(secondFocusController);
ReactTabFocus.focusFirst(nextController);
FocusControl.focusFirst(nextController);
expect(document.activeElement).toBe(divRef.current);
const previousController = ReactTabFocus.getPreviousController(
const previousController = FocusControl.getPreviousController(
nextController,
);
expect(previousController).toBe(firstFocusController);
ReactTabFocus.focusFirst(previousController);
FocusControl.focusFirst(previousController);
expect(document.activeElement).toBe(buttonRef.current);
});
});
@@ -16,7 +16,7 @@ describe('TabbableScope', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableScopeAPI = true;
TabbableScope = require('../TabbableScope').TabbableScope;
TabbableScope = require('../TabbableScope').default;
React = require('react');
});
+10
View File
@@ -687,9 +687,18 @@ const bundles = [
'react',
'react-interactions/events/keyboard',
'react-interactions/accessibility/tabbable-scope',
'react-interactions/accessibility/focus-control',
],
},
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: NON_FIBER_RENDERER,
entry: 'react-interactions/accessibility/focus-control',
global: 'ReactFocusControl',
externals: ['react'],
},
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: NON_FIBER_RENDERER,
@@ -703,6 +712,7 @@ const fbBundleExternalsMap = {
'react-interactions/events/keyboard': 'ReactEventsKeyboard',
'react-interactions/events/tap': 'ReactEventsTap',
'react-interactions/accessibility/tabbable-scope': 'ReactTabbableScope',
'react-interactions/accessibility/focus-control': 'ReactFocusControl',
};
// Based on deep-freeze by substack (public domain)