Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
});