@samitouri / QOS-React-2 / commits / 68a87eee54

[react-interactions] Add FocusList component (#16875)

Dominic Gannaway committed Sep 24, 2019 at 17:14 UTC 68a87eee544b2bcdff66c751e07cb55939b9d8ca
4 files changed +284 -2
packages/react-interactions/accessibility/src/FocusList.js new
+150
@@ -0,0 +1,150 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactScopeMethods} from 'shared/ReactTypes';
11 +import type {KeyboardEvent} from 'react-interactions/events/keyboard';
12 +
13 +import React from 'react';
14 +import {useKeyboard} from 'react-interactions/events/keyboard';
15 +
16 +type FocusItemProps = {
17 + children?: React.Node,
18 +};
19 +
20 +type FocusListProps = {|
21 + children: React.Node,
22 + portrait: boolean,
23 +|};
24 +
25 +const {useRef} = React;
26 +
27 +function focusListItem(cell: ReactScopeMethods): void {
28 + const tabbableNodes = cell.getScopedNodes();
29 + if (tabbableNodes !== null && tabbableNodes.length > 0) {
30 + tabbableNodes[0].focus();
31 + }
32 +}
33 +
34 +function getPreviousListItem(
35 + list: ReactScopeMethods,
36 + currentItem: ReactScopeMethods,
37 +): null | ReactScopeMethods {
38 + const items = list.getChildren();
39 + if (items !== null) {
40 + const currentItemIndex = items.indexOf(currentItem);
41 + if (currentItemIndex > 0) {
42 + return items[currentItemIndex - 1] || null;
43 + }
44 + }
45 + return null;
46 +}
47 +
48 +function getNextListItem(
49 + list: ReactScopeMethods,
50 + currentItem: ReactScopeMethods,
51 +): null | ReactScopeMethods {
52 + const items = list.getChildren();
53 + if (items !== null) {
54 + const currentItemIndex = items.indexOf(currentItem);
55 + if (currentItemIndex !== -1 && currentItemIndex !== items.length - 1) {
56 + return items[currentItemIndex + 1] || null;
57 + }
58 + }
59 + return null;
60 +}
61 +
62 +export function createFocusList(
63 + scopeImpl: (type: string, props: Object) => boolean,
64 +): Array<React.Component> {
65 + const TableScope = React.unstable_createScope(scopeImpl);
66 +
67 + function List({children, portrait}): FocusListProps {
68 + return (
69 + <TableScope type="list" portrait={portrait}>
70 + {children}
71 + </TableScope>
72 + );
73 + }
74 +
75 + function Item({children}): FocusItemProps {
76 + const scopeRef = useRef(null);
77 + const keyboard = useKeyboard({
78 + onKeyDown(event: KeyboardEvent): void {
79 + const currentItem = scopeRef.current;
80 + if (currentItem !== null) {
81 + const list = currentItem.getParent();
82 + const listProps = list && list.getProps();
83 + if (list !== null && listProps.type === 'list') {
84 + const portrait = listProps.portrait;
85 + switch (event.key) {
86 + case 'ArrowUp': {
87 + if (portrait) {
88 + const previousListItem = getPreviousListItem(
89 + list,
90 + currentItem,
91 + );
92 + if (previousListItem) {
93 + event.preventDefault();
94 + focusListItem(previousListItem);
95 + return;
96 + }
97 + }
98 + break;
99 + }
100 + case 'ArrowDown': {
101 + if (portrait) {
102 + const nextListItem = getNextListItem(list, currentItem);
103 + if (nextListItem) {
104 + event.preventDefault();
105 + focusListItem(nextListItem);
106 + return;
107 + }
108 + }
109 + break;
110 + }
111 + case 'ArrowLeft': {
112 + if (!portrait) {
113 + const previousListItem = getPreviousListItem(
114 + list,
115 + currentItem,
116 + );
117 + if (previousListItem) {
118 + event.preventDefault();
119 + focusListItem(previousListItem);
120 + return;
121 + }
122 + }
123 + break;
124 + }
125 + case 'ArrowRight': {
126 + if (!portrait) {
127 + const nextListItem = getNextListItem(list, currentItem);
128 + if (nextListItem) {
129 + event.preventDefault();
130 + focusListItem(nextListItem);
131 + return;
132 + }
133 + }
134 + break;
135 + }
136 + }
137 + }
138 + }
139 + event.continuePropagation();
140 + },
141 + });
142 + return (
143 + <TableScope listeners={keyboard} ref={scopeRef} type="item">
144 + {children}
145 + </TableScope>
146 + );
147 + }
148 +
149 + return [List, Item];
150 +}
packages/react-interactions/accessibility/src/FocusTable.js
+4 -1
@@ -149,6 +149,10 @@ export function createFocusTable(
149 const keyboard = useKeyboard({
150 onKeyDown(event: KeyboardEvent): void {
151 const currentCell = scopeRef.current;
152 + if (currentCell === null) {
153 + event.continuePropagation();
154 + return;
155 + }
156 switch (event.key) {
157 case 'ArrowUp': {
158 const [cells, cellIndex] = getRowCells(currentCell);
@@ -211,7 +215,6 @@ export function createFocusTable(
215 return;
216 }
217 }
214 - event.continuePropagation();
218 },
219 });
220 return (
packages/react-interactions/accessibility/src/__tests__/FocusList-test.internal.js new
+129
@@ -0,0 +1,129 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let createFocusList;
15 +let tabFocusableImpl;
16 +
17 +describe('FocusList', () => {
18 + beforeEach(() => {
19 + jest.resetModules();
20 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 + ReactFeatureFlags.enableScopeAPI = true;
22 + ReactFeatureFlags.enableFlareAPI = true;
23 + createFocusList = require('../FocusList').createFocusList;
24 + tabFocusableImpl = require('../TabbableScope').tabFocusableImpl;
25 + React = require('react');
26 + });
27 +
28 + describe('ReactDOM', () => {
29 + let ReactDOM;
30 + let container;
31 +
32 + beforeEach(() => {
33 + ReactDOM = require('react-dom');
34 + container = document.createElement('div');
35 + document.body.appendChild(container);
36 + });
37 +
38 + afterEach(() => {
39 + document.body.removeChild(container);
40 + container = null;
41 + });
42 +
43 + function createFocusListComponent() {
44 + const [FocusList, FocusItem] = createFocusList(tabFocusableImpl);
45 +
46 + return ({portrait}) => (
47 + <FocusList portrait={portrait}>
48 + <ul>
49 + <FocusItem>
50 + <li tabIndex={0}>Item 1</li>
51 + </FocusItem>
52 + <FocusItem>
53 + <li tabIndex={0}>Item 2</li>
54 + </FocusItem>
55 + <FocusItem>
56 + <li tabIndex={0}>Item 3</li>
57 + </FocusItem>
58 + </ul>
59 + </FocusList>
60 + );
61 + }
62 +
63 + it('handles keyboard arrow operations (portrait)', () => {
64 + const Test = createFocusListComponent();
65 +
66 + ReactDOM.render(<Test portrait={true} />, container);
67 + const listItems = document.querySelectorAll('li');
68 + const firstListItem = createEventTarget(listItems[0]);
69 + firstListItem.focus();
70 + firstListItem.keydown({
71 + key: 'ArrowDown',
72 + });
73 + expect(document.activeElement.textContent).toBe('Item 2');
74 +
75 + const secondListItem = createEventTarget(document.activeElement);
76 + secondListItem.keydown({
77 + key: 'ArrowDown',
78 + });
79 + expect(document.activeElement.textContent).toBe('Item 3');
80 +
81 + const thirdListItem = createEventTarget(document.activeElement);
82 + thirdListItem.keydown({
83 + key: 'ArrowDown',
84 + });
85 + expect(document.activeElement.textContent).toBe('Item 3');
86 + thirdListItem.keydown({
87 + key: 'ArrowRight',
88 + });
89 + expect(document.activeElement.textContent).toBe('Item 3');
90 + thirdListItem.keydown({
91 + key: 'ArrowLeft',
92 + });
93 + expect(document.activeElement.textContent).toBe('Item 3');
94 + });
95 +
96 + it('handles keyboard arrow operations (landscape)', () => {
97 + const Test = createFocusListComponent();
98 +
99 + ReactDOM.render(<Test portrait={false} />, container);
100 + const listItems = document.querySelectorAll('li');
101 + const firstListItem = createEventTarget(listItems[0]);
102 + firstListItem.focus();
103 + firstListItem.keydown({
104 + key: 'ArrowRight',
105 + });
106 + expect(document.activeElement.textContent).toBe('Item 2');
107 +
108 + const secondListItem = createEventTarget(document.activeElement);
109 + secondListItem.keydown({
110 + key: 'ArrowRight',
111 + });
112 + expect(document.activeElement.textContent).toBe('Item 3');
113 +
114 + const thirdListItem = createEventTarget(document.activeElement);
115 + thirdListItem.keydown({
116 + key: 'ArrowRight',
117 + });
118 + expect(document.activeElement.textContent).toBe('Item 3');
119 + thirdListItem.keydown({
120 + key: 'ArrowUp',
121 + });
122 + expect(document.activeElement.textContent).toBe('Item 3');
123 + thirdListItem.keydown({
124 + key: 'ArrowDown',
125 + });
126 + expect(document.activeElement.textContent).toBe('Item 3');
127 + });
128 + });
129 +});
packages/react-interactions/accessibility/src/__tests__/FocusTable-test.internal.js
+1 -1
@@ -14,7 +14,7 @@ let ReactFeatureFlags;
14 let createFocusTable;
15 let tabFocusableImpl;
16
17 -describe('ReactFocusTable', () => {
17 +describe('FocusTable', () => {
18 beforeEach(() => {
19 jest.resetModules();
20 ReactFeatureFlags = require('shared/ReactFeatureFlags');