Skip to content

Commit

Permalink
[react-interaction] Refactor a11y components more (#16866)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Sep 23, 2019
1 parent 1758b3f commit 1a6294d
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 180 deletions.
12 changes: 12 additions & 0 deletions packages/react-interactions/accessibility/focus-control.js
Original file line number Diff line number Diff line change
@@ -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');
139 changes: 139 additions & 0 deletions packages/react-interactions/accessibility/src/FocusControl.js
Original file line number Diff line number Diff line change
@@ -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];
}
7 changes: 4 additions & 3 deletions packages/react-interactions/accessibility/src/FocusTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 (
Expand Down
160 changes: 15 additions & 145 deletions packages/react-interactions/accessibility/src/TabFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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') {
Expand All @@ -191,7 +59,9 @@ export const TabFocusController = React.forwardRef(
}}
listeners={keyboard}>
{children}
</TabbableScope>
</Scope>
);
},
);

export default TabFocus;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading

0 comments on commit 1a6294d

Please sign in to comment.