Skip to content

Commit

Permalink
[react-interactions] Add tab handling to FocusList (#16958)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Sep 30, 2019
1 parent 10c7dfe commit ac8e8b3
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 38 deletions.
54 changes: 45 additions & 9 deletions packages/react-interactions/accessibility/src/FocusList.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {KeyboardEvent} from 'react-interactions/events/keyboard';

import React from 'react';
import {useKeyboard} from 'react-interactions/events/keyboard';
import {setElementCanTab} from 'react-interactions/accessibility/focus-control';

type FocusItemProps = {
children?: React.Node,
Expand All @@ -22,6 +23,7 @@ type FocusListProps = {|
children: React.Node,
portrait: boolean,
wrap?: boolean,
tabScope?: ReactScope,
|};

const {useRef} = React;
Expand All @@ -41,7 +43,7 @@ function getPreviousListItem(
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
const wrap = getListWrapProp(currentItem);
const wrap = getListProps(currentItem).wrap;
if (currentItemIndex === 0 && wrap) {
return items[items.length - 1] || null;
} else if (currentItemIndex > 0) {
Expand All @@ -58,7 +60,7 @@ function getNextListItem(
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
const wrap = getListWrapProp(currentItem);
const wrap = getListProps(currentItem).wrap;
const end = currentItemIndex === items.length - 1;
if (end && wrap) {
return items[0] || null;
Expand All @@ -69,22 +71,38 @@ function getNextListItem(
return null;
}

function getListWrapProp(currentItem: ReactScopeMethods): boolean {
const list = currentItem.getParent();
function getListProps(currentCell: ReactScopeMethods): Object {
const list = currentCell.getParent();
if (list !== null) {
const listProps = list.getProps();
return (listProps.type === 'list' && listProps.wrap) || false;
if (listProps && listProps.type === 'list') {
return listProps;
}
}
return false;
return {};
}

export function createFocusList(scope: ReactScope): Array<React.Component> {
const TableScope = React.unstable_createScope(scope.fn);

function List({children, portrait, wrap}): FocusListProps {
function List({
children,
portrait,
wrap,
tabScope: TabScope,
}): FocusListProps {
const tabScopeRef = useRef(null);
return (
<TableScope type="list" portrait={portrait} wrap={wrap}>
{children}
<TableScope
type="list"
portrait={portrait}
wrap={wrap}
tabScopeRef={tabScopeRef}>
{TabScope ? (
<TabScope ref={tabScopeRef}>{children}</TabScope>
) : (
children
)}
</TableScope>
);
}
Expand All @@ -100,6 +118,24 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
if (list !== null && listProps.type === 'list') {
const portrait = listProps.portrait;
switch (event.key) {
case 'Tab': {
const tabScope = getListProps(currentItem).tabScopeRef.current;
if (tabScope) {
const activeNode = document.activeElement;
const nodes = tabScope.getScopedNodes();
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;
}
case 'ArrowUp': {
if (portrait) {
const previousListItem = getPreviousListItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const FocusManager = React.forwardRef(
onBlurWithin: function(event) {
if (!containFocus) {
event.continuePropagation();
return;
}
const lastNode = event.target;
if (lastNode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type FocusTableProps = {|
focusTableByID: (id: string) => void,
) => void,
wrap?: boolean,
tabScope?: ReactScope,
|};

const {useRef} = React;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* @flow
*/

import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
import {
createEventTarget,
emulateBrowserTab,
} from 'react-interactions/events/src/dom/testing-library';

let React;
let ReactFeatureFlags;
Expand Down Expand Up @@ -156,5 +159,74 @@ describe('FocusList', () => {
});
expect(document.activeElement.textContent).toBe('Item 3');
});

it('handles keyboard arrow operations mixed with tabbing', () => {
const [FocusList, FocusItem] = createFocusList(TabbableScope);
const beforeRef = React.createRef();
const afterRef = React.createRef();

function Test() {
return (
<>
<input placeholder="Before" ref={beforeRef} />
<FocusList tabScope={TabbableScope} portrait={true}>
<ul>
<FocusItem>
<li>
<input placeholder="A" />
</li>
</FocusItem>
<FocusItem>
<li>
<input placeholder="B" />
</li>
</FocusItem>
<FocusItem>
<li>
<input placeholder="C" />
</li>
</FocusItem>
<FocusItem>
<li>
<input placeholder="D" />
</li>
</FocusItem>
<FocusItem>
<li>
<input placeholder="E" />
</li>
</FocusItem>
<FocusItem>
<li>
<input placeholder="F" />
</li>
</FocusItem>
</ul>
</FocusList>
<input placeholder="After" ref={afterRef} />
</>
);
}

ReactDOM.render(<Test />, container);
beforeRef.current.focus();

expect(document.activeElement.placeholder).toBe('Before');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('A');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('After');
emulateBrowserTab(true);
expect(document.activeElement.placeholder).toBe('A');
const a = createEventTarget(document.activeElement);
a.keydown({
key: 'ArrowDown',
});
expect(document.activeElement.placeholder).toBe('B');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('After');
emulateBrowserTab(true);
expect(document.activeElement.placeholder).toBe('B');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* @flow
*/

import {createEventTarget} from 'react-interactions/events/src/dom/testing-library';
import {
createEventTarget,
emulateBrowserTab,
} from 'react-interactions/events/src/dom/testing-library';

let React;
let ReactFeatureFlags;
Expand All @@ -29,33 +32,6 @@ describe('FocusTable', () => {
let ReactDOM;
let container;

function emulateBrowserTab(backwards) {
const activeElement = document.activeElement;
const focusedElem = createEventTarget(activeElement);
let defaultPrevented = false;
focusedElem.keydown({
key: 'Tab',
shiftKey: backwards,
preventDefault() {
defaultPrevented = true;
},
});
if (!defaultPrevented) {
// This is not a full spec compliant version, but should be suffice for this test
const focusableElems = Array.from(
document.querySelectorAll(
'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
),
).filter(
elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
);
const idx = focusableElems.indexOf(activeElement);
if (idx !== -1) {
focusableElems[backwards ? idx - 1 : idx + 1].focus();
}
}
}

beforeEach(() => {
ReactDOM = require('react-dom');
container = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,33 @@ function testWithPointerType(message, testFn) {
});
}

function emulateBrowserTab(backwards) {
const activeElement = document.activeElement;
const focusedElem = createEventTarget(activeElement);
let defaultPrevented = false;
focusedElem.keydown({
key: 'Tab',
shiftKey: backwards,
preventDefault() {
defaultPrevented = true;
},
});
if (!defaultPrevented) {
// This is not a full spec compliant version, but should be suffice for this test
const focusableElems = Array.from(
document.querySelectorAll(
'input, button, select, textarea, a[href], [tabindex], [contenteditable], iframe, object, embed',
),
).filter(
elem => elem.tabIndex > -1 && !elem.disabled && !elem.contentEditable,
);
const idx = focusableElems.indexOf(activeElement);
if (idx !== -1) {
focusableElems[backwards ? idx - 1 : idx + 1].focus();
}
}
}

export {
buttonsType,
createEventTarget,
Expand All @@ -166,4 +193,5 @@ export {
hasPointerEvent,
setPointerEvent,
testWithPointerType,
emulateBrowserTab,
};
1 change: 1 addition & 0 deletions scripts/rollup/bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ const bundles = [
'react',
'react-interactions/events/keyboard',
'react-interactions/accessibility/tabbable-scope',
'react-interactions/accessibility/focus-control',
],
},
];
Expand Down

0 comments on commit ac8e8b3

Please sign in to comment.