Skip to content

Commit

Permalink
[react-interactions] Add wrapping support to FocusList/FocusTable (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Sep 26, 2019
1 parent 49b0cb6 commit b9811ed
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 34 deletions.
46 changes: 32 additions & 14 deletions packages/react-interactions/accessibility/src/FocusList.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ import {useKeyboard} from 'react-interactions/events/keyboard';

type FocusItemProps = {
children?: React.Node,
onKeyDown?: KeyboardEvent => void,
};

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

const {useRef} = React;

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

Expand All @@ -38,7 +41,10 @@ function getPreviousListItem(
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
if (currentItemIndex > 0) {
const wrap = getListWrapProp(currentItem);
if (currentItemIndex === 0 && wrap) {
return items[items.length - 1] || null;
} else if (currentItemIndex > 0) {
return items[currentItemIndex - 1] || null;
}
}
Expand All @@ -52,25 +58,38 @@ function getNextListItem(
const items = list.getChildren();
if (items !== null) {
const currentItemIndex = items.indexOf(currentItem);
if (currentItemIndex !== -1 && currentItemIndex !== items.length - 1) {
const wrap = getListWrapProp(currentItem);
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 getListWrapProp(currentItem: ReactScopeMethods): boolean {
const list = currentItem.getParent();
if (list !== null) {
const listProps = list.getProps();
return (listProps.type === 'list' && listProps.wrap) || false;
}
return false;
}

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

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

function Item({children}): FocusItemProps {
function Item({children, onKeyDown}): FocusItemProps {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): void {
Expand All @@ -88,8 +107,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
currentItem,
);
if (previousListItem) {
event.preventDefault();
focusListItem(previousListItem);
focusListItem(previousListItem, event);
return;
}
}
Expand All @@ -99,8 +117,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
if (portrait) {
const nextListItem = getNextListItem(list, currentItem);
if (nextListItem) {
event.preventDefault();
focusListItem(nextListItem);
focusListItem(nextListItem, event);
return;
}
}
Expand All @@ -113,8 +130,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
currentItem,
);
if (previousListItem) {
event.preventDefault();
focusListItem(previousListItem);
focusListItem(previousListItem, event);
return;
}
}
Expand All @@ -124,8 +140,7 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
if (!portrait) {
const nextListItem = getNextListItem(list, currentItem);
if (nextListItem) {
event.preventDefault();
focusListItem(nextListItem);
focusListItem(nextListItem, event);
return;
}
}
Expand All @@ -134,6 +149,9 @@ export function createFocusList(scope: ReactScope): Array<React.Component> {
}
}
}
if (onKeyDown) {
onKeyDown(event);
}
event.continuePropagation();
},
});
Expand Down
78 changes: 62 additions & 16 deletions packages/react-interactions/accessibility/src/FocusTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {useKeyboard} from 'react-interactions/events/keyboard';

type FocusCellProps = {
children?: React.Node,
onKeyDown?: KeyboardEvent => void,
};

type FocusRowProps = {
Expand All @@ -28,6 +29,7 @@ type FocusTableProps = {|
direction: 'left' | 'right' | 'up' | 'down',
focusTableByID: (id: string) => void,
) => void,
wrap?: boolean,
|};

const {useRef} = React;
Expand All @@ -54,19 +56,26 @@ export function focusFirstCellOnTable(table: ReactScopeMethods): void {
}
}

function focusCell(cell: ReactScopeMethods): void {
function focusCell(cell: ReactScopeMethods, event?: KeyboardEvent): void {
const tabbableNodes = cell.getScopedNodes();
if (tabbableNodes !== null && tabbableNodes.length > 0) {
tabbableNodes[0].focus();
if (event) {
event.preventDefault();
}
}
}

function focusCellByIndex(row: ReactScopeMethods, cellIndex: number): void {
function focusCellByIndex(
row: ReactScopeMethods,
cellIndex: number,
event?: KeyboardEvent,
): void {
const cells = row.getChildren();
if (cells !== null) {
const cell = cells[cellIndex];
if (cell) {
focusCell(cell);
focusCell(cell, event);
}
}
}
Expand Down Expand Up @@ -130,12 +139,27 @@ function triggerNavigateOut(
event.continuePropagation();
}

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

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

function Table({children, onKeyboardOut, id}): FocusTableProps {
function Table({children, onKeyboardOut, id, wrap}): FocusTableProps {
return (
<TableScope type="table" onKeyboardOut={onKeyboardOut} id={id}>
<TableScope
type="table"
onKeyboardOut={onKeyboardOut}
id={id}
wrap={wrap}>
{children}
</TableScope>
);
Expand All @@ -145,7 +169,7 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
return <TableScope type="row">{children}</TableScope>;
}

function Cell({children}): FocusCellProps {
function Cell({children, onKeyDown}): FocusCellProps {
const scopeRef = useRef(null);
const keyboard = useKeyboard({
onKeyDown(event: KeyboardEvent): void {
Expand All @@ -162,10 +186,15 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
if (rows !== null) {
if (rowIndex > 0) {
const row = rows[rowIndex - 1];
focusCellByIndex(row, cellIndex);
event.preventDefault();
focusCellByIndex(row, cellIndex, event);
} else if (rowIndex === 0) {
triggerNavigateOut(currentCell, 'up', event);
const wrap = getTableWrapProp(currentCell);
if (wrap) {
const row = rows[rows.length - 1];
focusCellByIndex(row, cellIndex, event);
} else {
triggerNavigateOut(currentCell, 'up', event);
}
}
}
}
Expand All @@ -178,11 +207,16 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
if (rows !== null) {
if (rowIndex !== -1) {
if (rowIndex === rows.length - 1) {
triggerNavigateOut(currentCell, 'down', event);
const wrap = getTableWrapProp(currentCell);
if (wrap) {
const row = rows[0];
focusCellByIndex(row, cellIndex, event);
} else {
triggerNavigateOut(currentCell, 'down', event);
}
} else {
const row = rows[rowIndex + 1];
focusCellByIndex(row, cellIndex);
event.preventDefault();
focusCellByIndex(row, cellIndex, event);
}
}
}
Expand All @@ -196,7 +230,12 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
focusCell(cells[rowIndex - 1]);
event.preventDefault();
} else if (rowIndex === 0) {
triggerNavigateOut(currentCell, 'left', event);
const wrap = getTableWrapProp(currentCell);
if (wrap) {
focusCell(cells[cells.length - 1], event);
} else {
triggerNavigateOut(currentCell, 'left', event);
}
}
}
return;
Expand All @@ -206,16 +245,23 @@ export function createFocusTable(scope: ReactScope): Array<React.Component> {
if (cells !== null) {
if (rowIndex !== -1) {
if (rowIndex === cells.length - 1) {
triggerNavigateOut(currentCell, 'right', event);
const wrap = getTableWrapProp(currentCell);
if (wrap) {
focusCell(cells[0], event);
} else {
triggerNavigateOut(currentCell, 'right', event);
}
} else {
focusCell(cells[rowIndex + 1]);
event.preventDefault();
focusCell(cells[rowIndex + 1], event);
}
}
}
return;
}
}
if (onKeyDown) {
onKeyDown(event);
}
},
});
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ describe('FocusList', () => {
function createFocusListComponent() {
const [FocusList, FocusItem] = createFocusList(TabbableScope);

return ({portrait}) => (
<FocusList portrait={portrait}>
return ({portrait, wrap}) => (
<FocusList portrait={portrait} wrap={wrap}>
<ul>
<FocusItem>
<li tabIndex={0}>Item 1</li>
Expand Down Expand Up @@ -125,5 +125,36 @@ describe('FocusList', () => {
});
expect(document.activeElement.textContent).toBe('Item 3');
});

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

ReactDOM.render(<Test portrait={true} wrap={true} />, container);
const listItems = document.querySelectorAll('li');
let 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 1');

firstListItem = createEventTarget(document.activeElement);
firstListItem.keydown({
key: 'ArrowUp',
});
expect(document.activeElement.textContent).toBe('Item 3');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ describe('FocusTable', () => {
TabbableScope,
);

return ({onKeyboardOut, id}) => (
<FocusTable onKeyboardOut={onKeyboardOut} id={id}>
return ({onKeyboardOut, id, wrap}) => (
<FocusTable onKeyboardOut={onKeyboardOut} id={id} wrap={wrap}>
<table>
<tbody>
<FocusTableRow>
Expand Down Expand Up @@ -326,5 +326,36 @@ describe('FocusTable', () => {
});
expect(document.activeElement.placeholder).toBe('B1');
});

it('handles keyboard arrow operations with wrapping enabled', () => {
const Test = createFocusTableComponent();

ReactDOM.render(<Test wrap={true} />, container);
const buttons = document.querySelectorAll('button');
let a1 = createEventTarget(buttons[0]);
a1.focus();
a1.keydown({
key: 'ArrowRight',
});
expect(document.activeElement.textContent).toBe('A2');

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

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

a1 = createEventTarget(document.activeElement);
a1.keydown({
key: 'ArrowLeft',
});
expect(document.activeElement.textContent).toBe('A3');
});
});
});

0 comments on commit b9811ed

Please sign in to comment.