Skip to content

Commit

Permalink
[react-interactins] FocusTable tabScope handling+tabIndex control (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Sep 30, 2019
1 parent d3622d0 commit 10c7dfe
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 16 deletions.
39 changes: 39 additions & 0 deletions packages/react-interactions/accessibility/src/FocusControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,42 @@ export function getPreviousScope(
}
return allScopes[currentScopeIndex - 1];
}

const tabIndexDesc = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'tabIndex',
);
const tabIndexSetter = (tabIndexDesc: any).set;

export function setElementCanTab(elem: HTMLElement, canTab: boolean): void {
let tabIndexState = (elem: any)._tabIndexState;
if (!tabIndexState) {
tabIndexState = {
value: elem.tabIndex,
canTab,
};
(elem: any)._tabIndexState = tabIndexState;
if (!canTab) {
elem.tabIndex = -1;
}
// We track the tabIndex value so we can restore the correct
// tabIndex after we're done with it.
// $FlowFixMe: Flow comoplains that we are missing value?
Object.defineProperty(elem, 'tabIndex', {
enumerable: false,
configurable: true,
get() {
return tabIndexState.canTab ? tabIndexState.value : -1;
},
set(val) {
if (tabIndexState.canTab) {
tabIndexSetter.call(elem, val);
}
tabIndexState.value = val;
},
});
} else if (tabIndexState.canTab !== canTab) {
tabIndexSetter.call(elem, canTab ? tabIndexState.value : -1);
tabIndexState.canTab = canTab;
}
}
63 changes: 47 additions & 16 deletions packages/react-interactions/accessibility/src/FocusTable.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 FocusCellProps = {
children?: React.Node,
Expand Down Expand Up @@ -56,7 +57,7 @@ export function focusFirstCellOnTable(table: ReactScopeMethods): void {
}
}

function focusCell(cell: ReactScopeMethods, event?: KeyboardEvent): void {
function focusScope(cell: ReactScopeMethods, event?: KeyboardEvent): void {
const tabbableNodes = cell.getScopedNodes();
if (tabbableNodes !== null && tabbableNodes.length > 0) {
tabbableNodes[0].focus();
Expand All @@ -75,7 +76,7 @@ function focusCellByIndex(
if (cells !== null) {
const cell = cells[cellIndex];
if (cell) {
focusCell(cell, event);
focusScope(cell, event);
}
}
}
Expand Down Expand Up @@ -139,28 +140,40 @@ function triggerNavigateOut(
event.continuePropagation();
}

function getTableWrapProp(currentCell: ReactScopeMethods): boolean {
function getTableProps(currentCell: ReactScopeMethods): Object {
const row = currentCell.getParent();
if (row !== null && row.getProps().type === 'row') {
const table = row.getParent();
if (table !== null) {
return table.getProps().wrap || false;
return table.getProps();
}
}
return false;
return {};
}

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

function Table({children, onKeyboardOut, id, wrap}): FocusTableProps {
function Table({
children,
onKeyboardOut,
id,
wrap,
tabScope: TabScope,
}): FocusTableProps {
const tabScopeRef = useRef(null);
return (
<TableScope
type="table"
onKeyboardOut={onKeyboardOut}
id={id}
wrap={wrap}>
{children}
wrap={wrap}
tabScopeRef={tabScopeRef}>
{TabScope ? (
<TabScope ref={tabScopeRef}>{children}</TabScope>
) : (
children
)}
</TableScope>
);
}
Expand All @@ -179,6 +192,24 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
return;
}
switch (event.key) {
case 'Tab': {
const tabScope = getTableProps(currentCell).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': {
const [cells, cellIndex] = getRowCells(currentCell);
if (cells !== null) {
Expand All @@ -188,7 +219,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
const row = rows[rowIndex - 1];
focusCellByIndex(row, cellIndex, event);
} else if (rowIndex === 0) {
const wrap = getTableWrapProp(currentCell);
const wrap = getTableProps(currentCell).wrap;
if (wrap) {
const row = rows[rows.length - 1];
focusCellByIndex(row, cellIndex, event);
Expand All @@ -207,7 +238,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
if (rows !== null) {
if (rowIndex !== -1) {
if (rowIndex === rows.length - 1) {
const wrap = getTableWrapProp(currentCell);
const wrap = getTableProps(currentCell).wrap;
if (wrap) {
const row = rows[0];
focusCellByIndex(row, cellIndex, event);
Expand All @@ -227,12 +258,12 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
const [cells, rowIndex] = getRowCells(currentCell);
if (cells !== null) {
if (rowIndex > 0) {
focusCell(cells[rowIndex - 1]);
focusScope(cells[rowIndex - 1]);
event.preventDefault();
} else if (rowIndex === 0) {
const wrap = getTableWrapProp(currentCell);
const wrap = getTableProps(currentCell).wrap;
if (wrap) {
focusCell(cells[cells.length - 1], event);
focusScope(cells[cells.length - 1], event);
} else {
triggerNavigateOut(currentCell, 'left', event);
}
Expand All @@ -245,14 +276,14 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
if (cells !== null) {
if (rowIndex !== -1) {
if (rowIndex === cells.length - 1) {
const wrap = getTableWrapProp(currentCell);
const wrap = getTableProps(currentCell).wrap;
if (wrap) {
focusCell(cells[0], event);
focusScope(cells[0], event);
} else {
triggerNavigateOut(currentCell, 'right', event);
}
} else {
focusCell(cells[rowIndex + 1], event);
focusScope(cells[rowIndex + 1], event);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@ 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 Expand Up @@ -357,5 +384,68 @@ describe('FocusTable', () => {
});
expect(document.activeElement.textContent).toBe('A3');
});

it('handles keyboard arrow operations mixed with tabbing', () => {
const [FocusTable, FocusRow, FocusCell] = createFocusTable(TabbableScope);
const beforeRef = React.createRef();
const afterRef = React.createRef();

function Test() {
return (
<>
<input placeholder="Before" ref={beforeRef} />
<FocusTable tabScope={TabbableScope}>
<div>
<FocusRow>
<FocusCell>
<input placeholder="A1" />
</FocusCell>
<FocusCell>
<input placeholder="B1" />
</FocusCell>
<FocusCell>
<input placeholder="C1" />
</FocusCell>
</FocusRow>
</div>
<div>
<FocusRow>
<FocusCell>
<input placeholder="A2" />
</FocusCell>
<FocusCell>
<input placeholder="B2" />
</FocusCell>
<FocusCell>
<input placeholder="C1" />
</FocusCell>
</FocusRow>
</div>
</FocusTable>
<input placeholder="After" ref={afterRef} />
</>
);
}

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

expect(document.activeElement.placeholder).toBe('Before');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('A1');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('After');
emulateBrowserTab(true);
expect(document.activeElement.placeholder).toBe('A1');
const a1 = createEventTarget(document.activeElement);
a1.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.placeholder).toBe('B1');
emulateBrowserTab();
expect(document.activeElement.placeholder).toBe('After');
emulateBrowserTab(true);
expect(document.activeElement.placeholder).toBe('B1');
});
});
});
1 change: 1 addition & 0 deletions scripts/rollup/bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ const bundles = [
'react',
'react-interactions/events/keyboard',
'react-interactions/accessibility/tabbable-scope',
'react-interactions/accessibility/focus-control',
],
},

Expand Down

0 comments on commit 10c7dfe

Please sign in to comment.