Files
react/packages/react-interactions/accessibility/src/FocusGroup.js

231 lines
6.8 KiB
JavaScript

/**
* 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';
import React from 'react';
import {useKeyboard} from 'react-interactions/events/keyboard';
import setElementCanTab from './shared/setElementCanTab';
type FocusItemProps = {
children?: React.Node,
onKeyDown?: KeyboardEvent => void,
};
type FocusGroupProps = {|
children: React.Node,
portrait: boolean,
wrap?: boolean,
tabScopeQuery?: (type: string | Object, props: Object) => boolean,
allowModifiers?: boolean,
|};
const {useRef} = React;
function focusGroupItem(
scopeQuery: (type: string | Object, props: Object) => boolean,
cell: ReactScopeMethods,
event: KeyboardEvent,
): void {
const firstScopedNode = cell.queryFirstNode(scopeQuery);
if (firstScopedNode !== null) {
firstScopedNode.focus();
event.preventDefault();
}
}
function getPreviousGroupItem(
group: ReactScopeMethods,
currentItem: ReactScopeMethods,
): null | ReactScopeMethods {
const items = group.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
const wrap = getGroupProps(currentItem).wrap;
if (currentItemIndex === 0 && wrap) {
return items[items.length - 1] || null;
} else if (currentItemIndex > 0) {
return items[currentItemIndex - 1] || null;
}
}
return null;
}
function getNextGroupItem(
group: ReactScopeMethods,
currentItem: ReactScopeMethods,
): null | ReactScopeMethods {
const items = group.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
const wrap = getGroupProps(currentItem).wrap;
const end = currentItemIndex === items.length - 1;
if (end && wrap) {
return items[0] || null;
} else if (currentItemIndex !== -1 && !end) {
return items[currentItemIndex + 1] || null;
}
}
return null;
}
function getGroupProps(currentCell: ReactScopeMethods): Object {
const group = currentCell.getParent();
if (group !== null) {
const groupProps = group.getProps();
if (groupProps && groupProps.type === 'group') {
return groupProps;
}
}
return {};
}
function hasModifierKey(event: KeyboardEvent): boolean {
const {altKey, ctrlKey, metaKey, shiftKey} = event;
return (
altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
);
}
export function createFocusGroup(
scopeQuery: (type: string | Object, props: Object) => boolean,
): [(FocusGroupProps) => React.Node, (FocusItemProps) => React.Node] {
const TableScope = React.unstable_createScope();
function Group({
children,
portrait,
wrap,
tabScopeQuery,
allowModifiers,
}: FocusGroupProps): React.Node {
return (
<TableScope
type="group"
portrait={portrait}
wrap={wrap}
tabScopeQuery={tabScopeQuery}
allowModifiers={allowModifiers}>
{children}
</TableScope>
);
}
function Item({children, onKeyDown}: FocusItemProps): React.Node {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): void {
const currentItem = scopeRef.current;
if (currentItem !== null) {
const group = currentItem.getParent();
const groupProps = group && group.getProps();
if (group !== null && groupProps.type === 'group') {
const portrait = groupProps.portrait;
const key = event.key;
if (key === 'Tab') {
const tabScopeQuery = getGroupProps(currentItem).tabScopeQuery;
if (tabScopeQuery) {
const groupScope = currentItem.getParent();
if (groupScope) {
const activeNode = document.activeElement;
const nodes = groupScope.queryAllNodes(tabScopeQuery);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node !== activeNode) {
setElementCanTab(node, false);
} else {
setElementCanTab(node, true);
}
}
return;
}
}
event.continuePropagation();
return;
}
// Using modifier keys with keyboard arrow events should be no-ops
// unless an explicit allowModifiers flag is set on the FocusGroup.
if (hasModifierKey(event)) {
const allowModifiers = getGroupProps(currentItem).allowModifiers;
if (!allowModifiers) {
event.continuePropagation();
return;
}
}
switch (key) {
case 'ArrowUp': {
if (portrait) {
const previousGroupItem = getPreviousGroupItem(
group,
currentItem,
);
if (previousGroupItem) {
focusGroupItem(scopeQuery, previousGroupItem, event);
return;
}
}
break;
}
case 'ArrowDown': {
if (portrait) {
const nextGroupItem = getNextGroupItem(group, currentItem);
if (nextGroupItem) {
focusGroupItem(scopeQuery, nextGroupItem, event);
return;
}
}
break;
}
case 'ArrowLeft': {
if (!portrait) {
const previousGroupItem = getPreviousGroupItem(
group,
currentItem,
);
if (previousGroupItem) {
focusGroupItem(scopeQuery, previousGroupItem, event);
return;
}
}
break;
}
case 'ArrowRight': {
if (!portrait) {
const nextGroupItem = getNextGroupItem(group, currentItem);
if (nextGroupItem) {
focusGroupItem(scopeQuery, nextGroupItem, event);
return;
}
}
break;
}
}
}
}
if (onKeyDown) {
onKeyDown(event);
}
event.continuePropagation();
},
});
return (
<TableScope
DEPRECATED_flareListeners={keyboard}
ref={scopeRef}
type="item">
{children}
</TableScope>
);
}
return [Group, Item];
}