Skip to content

Commit

Permalink
[react-interactions] Add FocusList component (#16875)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Sep 24, 2019
1 parent 18d2e0c commit 68a87ee
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 2 deletions.
150 changes: 150 additions & 0 deletions packages/react-interactions/accessibility/src/FocusList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* 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';

type FocusItemProps = {
children?: React.Node,
};

type FocusListProps = {|
children: React.Node,
portrait: boolean,
|};

const {useRef} = React;

function focusListItem(cell: ReactScopeMethods): void {
const tabbableNodes = cell.getScopedNodes();
if (tabbableNodes !== null && tabbableNodes.length > 0) {
tabbableNodes[0].focus();
}
}

function getPreviousListItem(
list: ReactScopeMethods,
currentItem: ReactScopeMethods,
): null | ReactScopeMethods {
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
if (currentItemIndex > 0) {
return items[currentItemIndex - 1] || null;
}
}
return null;
}

function getNextListItem(
list: ReactScopeMethods,
currentItem: ReactScopeMethods,
): null | ReactScopeMethods {
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
if (currentItemIndex !== -1 && currentItemIndex !== items.length - 1) {
return items[currentItemIndex + 1] || null;
}
}
return null;
}

export function createFocusList(
scopeImpl: (type: string, props: Object) => boolean,
): Array<React.Component> {
const TableScope = React.unstable_createScope(scopeImpl);

function List({children, portrait}): FocusListProps {
return (
<TableScope type="list" portrait={portrait}>
{children}
</TableScope>
);
}

function Item({children}): FocusItemProps {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): void {
const currentItem = scopeRef.current;
if (currentItem !== null) {
const list = currentItem.getParent();
const listProps = list && list.getProps();
if (list !== null && listProps.type === 'list') {
const portrait = listProps.portrait;
switch (event.key) {
case 'ArrowUp': {
if (portrait) {
const previousListItem = getPreviousListItem(
list,
currentItem,
);
if (previousListItem) {
event.preventDefault();
focusListItem(previousListItem);
return;
}
}
break;
}
case 'ArrowDown': {
if (portrait) {
const nextListItem = getNextListItem(list, currentItem);
if (nextListItem) {
event.preventDefault();
focusListItem(nextListItem);
return;
}
}
break;
}
case 'ArrowLeft': {
if (!portrait) {
const previousListItem = getPreviousListItem(
list,
currentItem,
);
if (previousListItem) {
event.preventDefault();
focusListItem(previousListItem);
return;
}
}
break;
}
case 'ArrowRight': {
if (!portrait) {
const nextListItem = getNextListItem(list, currentItem);
if (nextListItem) {
event.preventDefault();
focusListItem(nextListItem);
return;
}
}
break;
}
}
}
}
event.continuePropagation();
},
});
return (
<TableScope listeners={keyboard} ref={scopeRef} type="item">
{children}
</TableScope>
);
}

return [List, Item];
}
5 changes: 4 additions & 1 deletion packages/react-interactions/accessibility/src/FocusTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export function createFocusTable(
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): void {
const currentCell = scopeRef.current;
if (currentCell === null) {
event.continuePropagation();
return;
}
switch (event.key) {
case 'ArrowUp': {
const [cells, cellIndex] = getRowCells(currentCell);
Expand Down Expand Up @@ -211,7 +215,6 @@ export function createFocusTable(
return;
}
}
event.continuePropagation();
},
});
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* 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 {createEventTarget} from 'react-interactions/events/src/dom/testing-library';

let React;
let ReactFeatureFlags;
let createFocusList;
let tabFocusableImpl;

describe('FocusList', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableScopeAPI = true;
ReactFeatureFlags.enableFlareAPI = true;
createFocusList = require('../FocusList').createFocusList;
tabFocusableImpl = require('../TabbableScope').tabFocusableImpl;
React = require('react');
});

describe('ReactDOM', () => {
let ReactDOM;
let container;

beforeEach(() => {
ReactDOM = require('react-dom');
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
container = null;
});

function createFocusListComponent() {
const [FocusList, FocusItem] = createFocusList(tabFocusableImpl);

return ({portrait}) => (
<FocusList portrait={portrait}>
<ul>
<FocusItem>
<li tabIndex={0}>Item 1</li>
</FocusItem>
<FocusItem>
<li tabIndex={0}>Item 2</li>
</FocusItem>
<FocusItem>
<li tabIndex={0}>Item 3</li>
</FocusItem>
</ul>
</FocusList>
);
}

it('handles keyboard arrow operations (portrait)', () => {
const Test = createFocusListComponent();

ReactDOM.render(<Test portrait={true} />, container);
const listItems = document.querySelectorAll('li');
const firstListItem = createEventTarget(listItems[0]);
firstListItem.focus();
firstListItem.keydown({
key: 'ArrowDown',
});
expect(document.activeElement.textContent).toBe('Item 2');

const secondListItem = createEventTarget(document.activeElement);
secondListItem.keydown({
key: 'ArrowDown',
});
expect(document.activeElement.textContent).toBe('Item 3');

const thirdListItem = createEventTarget(document.activeElement);
thirdListItem.keydown({
key: 'ArrowDown',
});
expect(document.activeElement.textContent).toBe('Item 3');
thirdListItem.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.textContent).toBe('Item 3');
thirdListItem.keydown({
key: 'ArrowLeft',
});
expect(document.activeElement.textContent).toBe('Item 3');
});

it('handles keyboard arrow operations (landscape)', () => {
const Test = createFocusListComponent();

ReactDOM.render(<Test portrait={false} />, container);
const listItems = document.querySelectorAll('li');
const firstListItem = createEventTarget(listItems[0]);
firstListItem.focus();
firstListItem.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.textContent).toBe('Item 2');

const secondListItem = createEventTarget(document.activeElement);
secondListItem.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.textContent).toBe('Item 3');

const thirdListItem = createEventTarget(document.activeElement);
thirdListItem.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.textContent).toBe('Item 3');
thirdListItem.keydown({
key: 'ArrowUp',
});
expect(document.activeElement.textContent).toBe('Item 3');
thirdListItem.keydown({
key: 'ArrowDown',
});
expect(document.activeElement.textContent).toBe('Item 3');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let ReactFeatureFlags;
let createFocusTable;
let tabFocusableImpl;

describe('ReactFocusTable', () => {
describe('FocusTable', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
Expand Down

0 comments on commit 68a87ee

Please sign in to comment.