Skip to content

Commit

Permalink
fix: support keyboard navigation (arrow key up/down) in grid mode (#1231
Browse files Browse the repository at this point in the history
)
  • Loading branch information
quanho committed Apr 24, 2023
1 parent 557ba46 commit a3fafd2
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Frequency from './components/Frequency';
import ItemGrid from './components/ItemGrid';
import getCellFromPages from './helpers/get-cell-from-pages';
import { getValueLabel } from '../ScreenReaders';
import getRowsKeyboardNavigation from '../../interactions/keyboard-navigation/keybord-nav-rows';
import getRowsKeyboardNavigation from '../../interactions/keyboard-navigation/keyboard-nav-rows';

function RowColumn({ index, rowIndex, columnIndex, style, data }) {
const {
Expand Down Expand Up @@ -102,8 +102,18 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) {
}, [rowRef, focusListItems.first, focusListItems.last]);

const handleKeyDownCallback = useCallback(
getRowsKeyboardNavigation({ ...actions, focusListItems, keyboard, isModal }),
[actions, keyboard?.innerTabStops]
getRowsKeyboardNavigation({
...actions,
focusListItems,
keyboard,
isModal,
rowCount,
columnCount,
rowIndex,
columnIndex,
layoutOrder,
}),
[actions, keyboard?.innerTabStops, rowCount, columnCount, rowIndex, columnIndex, layoutOrder]
);

const cell = useMemo(() => getCellFromPages({ pages, cellIndex }), [pages, cellIndex]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import renderer from 'react-test-renderer';
import { Grid, Typography } from '@mui/material';
import { createTheme, ThemeProvider } from '@nebula.js/ui/theme';
import Lock from '@nebula.js/ui/icons/lock';
import * as rowsKeyboardNavigation from '../../../interactions/keyboard-navigation/keybord-nav-rows';
import * as rowsKeyboardNavigation from '../../../interactions/keyboard-navigation/keyboard-nav-rows';
import ListBoxCheckbox from '../components/ListBoxCheckbox';
import * as screenReaders from '../../ScreenReaders';
import ListBoxRadioButton from '../components/ListBoxRadioButton';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import KEYS from '../../../../../keys';
import findNextItemIndex from '../find-next-item-index';

describe('find next item index to focus on key up and key down', () => {
const numCells = 12;
let rowCount;
let columnCount;
let layoutOrder;
let keyCode;

describe('key down', () => {
beforeEach(() => {
keyCode = KEYS.ARROW_DOWN;
});
describe('row layout', () => {
beforeEach(() => {
layoutOrder = 'row';
columnCount = 5;
rowCount = 3;
});
test('should return correct index for rowIndex 0', () => {
const rowIndex = 0;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(5);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(6);
expect(
findNextItemIndex({ rowIndex, columnIndex: 2, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(7);
expect(
findNextItemIndex({ rowIndex, columnIndex: 3, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(8);
expect(
findNextItemIndex({ rowIndex, columnIndex: 4, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(9);
});

test('should return correct index for rowIndex 1', () => {
const rowIndex = 1;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(10);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(11);
expect(
findNextItemIndex({ rowIndex, columnIndex: 2, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(3);
expect(
findNextItemIndex({ rowIndex, columnIndex: 3, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(4);
expect(
findNextItemIndex({ rowIndex, columnIndex: 4, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(-1);
});

test('should return correct index for rowIndex 2', () => {
const rowIndex = 2;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(1);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(2);
});
});

describe('column layout', () => {
beforeEach(() => {
layoutOrder = 'column';
columnCount = 3;
rowCount = 5;
});
test('should return correct index for columnIndex 0', () => {
const columnIndex = 0;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(3);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(6);
expect(
findNextItemIndex({ rowIndex: 2, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(8);
expect(
findNextItemIndex({ rowIndex: 3, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(10);
expect(
findNextItemIndex({ rowIndex: 4, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(1);
});

test('should return correct index for columnIndex 1', () => {
const columnIndex = 1;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(4);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(7);
expect(
findNextItemIndex({ rowIndex: 2, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(9);
expect(
findNextItemIndex({ rowIndex: 3, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(11);
expect(
findNextItemIndex({ rowIndex: 4, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(2);
});

test('should return correct index for columnIndex 2', () => {
const columnIndex = 2;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(5);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(-1);
});
});
});

describe('key up', () => {
beforeEach(() => {
keyCode = KEYS.ARROW_UP;
});
describe('row layout', () => {
beforeEach(() => {
layoutOrder = 'row';
columnCount = 5;
rowCount = 3;
});
test('should return correct index for rowIndex 0', () => {
const rowIndex = 0;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(-1);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(10);
expect(
findNextItemIndex({ rowIndex, columnIndex: 2, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(11);
expect(
findNextItemIndex({ rowIndex, columnIndex: 3, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(7);
expect(
findNextItemIndex({ rowIndex, columnIndex: 4, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(8);
});

test('should return correct index for rowIndex 1', () => {
const rowIndex = 1;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(0);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(1);
expect(
findNextItemIndex({ rowIndex, columnIndex: 2, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(2);
expect(
findNextItemIndex({ rowIndex, columnIndex: 3, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(3);
expect(
findNextItemIndex({ rowIndex, columnIndex: 4, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(4);
});

test('should return correct index for rowIndex 2', () => {
const rowIndex = 2;
expect(
findNextItemIndex({ rowIndex, columnIndex: 0, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(5);
expect(
findNextItemIndex({ rowIndex, columnIndex: 1, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(6);
});
});

describe('column layout', () => {
beforeEach(() => {
layoutOrder = 'column';
columnCount = 3;
rowCount = 5;
});
test('should return correct index for columnIndex 0', () => {
const columnIndex = 0;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(-1);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(0);
expect(
findNextItemIndex({ rowIndex: 2, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(3);
expect(
findNextItemIndex({ rowIndex: 3, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(6);
expect(
findNextItemIndex({ rowIndex: 4, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(8);
});

test('should return correct index for columnIndex 1', () => {
const columnIndex = 1;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(10);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(1);
expect(
findNextItemIndex({ rowIndex: 2, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(4);
expect(
findNextItemIndex({ rowIndex: 3, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(7);
expect(
findNextItemIndex({ rowIndex: 4, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(9);
});

test('should return correct index for columnIndex 2', () => {
const columnIndex = 2;
expect(
findNextItemIndex({ rowIndex: 0, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(11);
expect(
findNextItemIndex({ rowIndex: 1, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells })
).toEqual(2);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import KEYS from '../../../../../keys';
import getRowsKeyboardNavigation from '../keybord-nav-rows';
import getRowsKeyboardNavigation from '../keyboard-nav-rows';
import { createElement } from './keyboard-nav-test-utils';

describe('keyboard navigation', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import KEYS from '../../../../keys';

// Find next item index to focus in the dom element list on key up and key down
const findNextItemIndex = ({ rowIndex, columnIndex, rowCount, columnCount, layoutOrder, keyCode, numCells }) => {
const getNumCellsInColumn = (colIdx) => {
let remain;
if (layoutOrder === 'row') {
remain = numCells % columnCount;
return colIdx < remain ? rowCount : rowCount - 1;
}
remain = numCells % rowCount;
return colIdx < columnCount - 1 ? rowCount : remain;
};

let nextRowIndex = rowIndex;
let nextColumnIndex = columnIndex;
if (keyCode === KEYS.ARROW_DOWN) {
if (rowIndex >= getNumCellsInColumn(columnIndex) - 1) {
if (columnIndex === columnCount - 1) return -1;
nextRowIndex = 0;
nextColumnIndex = columnIndex + 1;
} else {
nextRowIndex = rowIndex + 1;
}
} else if (rowIndex === 0) {
if (columnIndex === 0) return -1;
nextRowIndex = getNumCellsInColumn(columnIndex - 1) - 1;
nextColumnIndex = columnIndex - 1;
} else {
nextRowIndex = rowIndex - 1;
}

// Convert from row, column indices to the element index in the dom element list
if (layoutOrder === 'row') {
return nextRowIndex * columnCount + nextColumnIndex;
}

// The dom element list is always row order. If the layout is column order then the conversion is not straight forward
const remain = numCells % rowCount;
if (remain === 0 || nextRowIndex < remain) return nextRowIndex * columnCount + nextColumnIndex;
return (nextRowIndex - remain) * (columnCount - 1) + nextColumnIndex + remain * columnCount;
};

export default findNextItemIndex;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import KEYS from '../../../../keys';
import { focusSearch, getElementIndex } from './keyboard-nav-methods';
import findNextItemIndex from './find-next-item-index';

export default function getRowsKeyboardNavigation({
select,
Expand All @@ -11,10 +12,37 @@ export default function getRowsKeyboardNavigation({
focusListItems,
keyboard,
isModal,
rowCount,
columnCount,
rowIndex,
columnIndex,
layoutOrder,
}) {
const getElement = (elm, next = false) => {
const parentElm = elm && elm.parentElement[next ? 'nextElementSibling' : 'previousElementSibling'];
return parentElm && parentElm.querySelector('[role]');
const getElement = (keyCode, elm, next = false) => {
if (
keyCode === KEYS.ARROW_LEFT ||
keyCode === KEYS.ARROW_RIGHT ||
!(typeof rowIndex === 'number' && typeof columnIndex === 'number')
) {
const parentElm = elm?.parentElement[next ? 'nextElementSibling' : 'previousElementSibling'];
return parentElm?.querySelector('[role]');
}
const gridElm = elm?.parentElement.parentElement;
const numCells = gridElm?.childElementCount;
if (numCells) {
const nextIndex = findNextItemIndex({
rowIndex,
columnIndex,
rowCount,
columnCount,
layoutOrder,
keyCode,
numCells,
});
const nextElm = elm?.parentElement.parentElement.children[nextIndex];
return nextElm?.querySelector('[role]');
}
return undefined;
};

let startedRange = false;
Expand Down Expand Up @@ -64,7 +92,7 @@ export default function getRowsKeyboardNavigation({
break;
case KEYS.ARROW_DOWN:
case KEYS.ARROW_RIGHT:
elementToFocus = getElement(currentTarget, true);
elementToFocus = getElement(keyCode, currentTarget, true);
if (shiftKey && elementToFocus) {
if (startedRange) {
select([getElementIndex(currentTarget)], true);
Expand All @@ -75,7 +103,7 @@ export default function getRowsKeyboardNavigation({
break;
case KEYS.ARROW_UP:
case KEYS.ARROW_LEFT:
elementToFocus = getElement(currentTarget, false);
elementToFocus = getElement(keyCode, currentTarget, false);
if (shiftKey && elementToFocus) {
if (startedRange) {
select([getElementIndex(currentTarget)], true);
Expand Down

0 comments on commit a3fafd2

Please sign in to comment.