Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Commit

Permalink
Allow break to the next/previous visual row on right/left arrow key. (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
adoroshk committed Apr 25, 2024
1 parent 989c2e1 commit 05f7d4f
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 61 deletions.
3 changes: 3 additions & 0 deletions packages/terra-compact-interactive-list/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Changed
* Keyboard navigation updated to wrap to the next/previous line at the end/start of a visual row.

## 1.20.0 - (April 23, 2024)

* Changed
Expand Down
102 changes: 67 additions & 35 deletions packages/terra-compact-interactive-list/src/utils/keyHandlerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,29 @@ const getFirstSemanticRowIndexInVisualRow = (rowsLength, numberOfColumns, flowHo
return firstItemInVisualRow;
};

/**
* Finds the last semantic row index in a visual row, where the given row is located
* @param {number} rowsLength - a total number of seamntic rows in the list.
* @param {number} numberOfColumns - a number of visual columns.
* @param {boolean} flowHorizontally - sematic rows horizontal flow direction
* @param {number} row - an index of the currently focused semantic row.
* @returns - the index of the last semantic row in the same visual row as currently focused row.
*/
const getLastSemanticRowIndexInVisualRow = (rowsLength, numberOfColumns, flowHorizontally, row) => {
if (row === undefined || row === null) {
// If current row omitted, return the index of the last element
return rowsLength - 1;
}
if (flowHorizontally) {
const lastItemInVisualRow = (Math.ceil((row + 1) / numberOfColumns) * numberOfColumns) - 1;
return lastItemInVisualRow < rowsLength - 1 ? lastItemInVisualRow : rowsLength - 1;
}
const rowsPerColumn = Math.ceil(rowsLength / numberOfColumns);
const rowsToTop = row % rowsPerColumn;
const lastItemInVisualRow = (numberOfColumns - 1) * rowsPerColumn + rowsToTop;
return lastItemInVisualRow < rowsLength ? lastItemInVisualRow : lastItemInVisualRow - rowsPerColumn;
};

/**
* Calculates new semantic row and cell indexes to focus on per LEFT ARROW KEY press.
* @param {KeyboardEvent} event - keyboard event.
Expand All @@ -156,15 +179,28 @@ export const handleLeftKey = (event, focusedCell, numberOfColumns, flowHorizonta
// Focus moves to the first cell in the first item in the visual row.
return { row: firstItemInVisualRow, cell: 0 };
}
// Focus should go till the start of the visual row, and should not break to the previous visual row.

if (cell === 0) {
// Focus reached the beginning of the the semantic row.
// Check if focus reached the beginning of the visual row.
let nextCell = cell;
let nextRow = row;
if (row === 0 || row === firstItemInVisualRow) {
// The first item in the list, or the first item in the visual row.
// Focus should stay where it is.
if (row === 0) {
// The first item in the list. Focus should stay where it is.
return focusedCell;
}
if (!flowHorizontally) {
// VERTICAL FLOW
// Check if the first semantic row in a VISUAL row has been reached.
const rowsPerColumn = Math.ceil(rowsLength / numberOfColumns);
if (row < rowsPerColumn) {
// Focus should wrap to the previous visual row.
const rowsToTop = row % rowsPerColumn;
const previousVisualRow = rowsToTop - 1;
const previousVisualRowlastSemanticRowIndex = getLastSemanticRowIndexInVisualRow(rowsLength, numberOfColumns, flowHorizontally, previousVisualRow);
return { row: previousVisualRowlastSemanticRowIndex, cell: cellsLength - 1 };
}
}
// The first cell. Focus moves to the last cell of the semantic row to the left.
nextCell = cellsLength - 1;
nextRow -= flowHorizontally ? 1 : Math.ceil(rowsLength / numberOfColumns);
Expand All @@ -174,29 +210,6 @@ export const handleLeftKey = (event, focusedCell, numberOfColumns, flowHorizonta
return { row, cell: cell - 1 };
};

/**
* Finds the last semantic row index in a visual row, where the given row is located
* @param {number} rowsLength - a total number of seamntic rows in the list.
* @param {number} numberOfColumns - a number of visual columns.
* @param {boolean} flowHorizontally - sematic rows horizontal flow direction
* @param {number} row - an index of the currently focused semantic row.
* @returns - the index of the last semantic row in the same visual row as currently focused row.
*/
const getLastSemanticRowIndexInVisualRow = (rowsLength, numberOfColumns, flowHorizontally, row) => {
if (row === undefined || row === null) {
// If current row omitted, return the index of the last element
return rowsLength - 1;
}
if (flowHorizontally) {
const lastItemInVisualRow = (Math.ceil((row + 1) / numberOfColumns) * numberOfColumns) - 1;
return lastItemInVisualRow < rowsLength - 1 ? lastItemInVisualRow : rowsLength - 1;
}
const rowsPerColumn = Math.ceil(rowsLength / numberOfColumns);
const rowsToTop = row % rowsPerColumn;
const lastItemInVisualRow = (numberOfColumns - 1) * rowsPerColumn + rowsToTop;
return lastItemInVisualRow < rowsLength ? lastItemInVisualRow : lastItemInVisualRow - rowsPerColumn;
};

/**
* Calculates new semantic row and cell indexes to focus on per RIGHT ARROW KEY press.
* @param {KeyboardEvent} event - keyboard event.
Expand Down Expand Up @@ -229,17 +242,36 @@ export const handleRightKey = (event, focusedCell, numberOfColumns, flowHorizont
nextRow = getLastSemanticRowIndexInVisualRow(rowsLength, numberOfColumns, flowHorizontally, row);
return { row: nextRow, cell: nextCell };
}
// Focus should go till the end of the visual row, and should not break to the next visual row.

if (cell === (cellsLength - 1)) {
// The last semantic column in the row.
// Check if the last item in visual row.
const lastRowIndex = getLastSemanticRowIndexInVisualRow(rowsLength, numberOfColumns, flowHorizontally, row);
if (row === lastRowIndex) {
// The last item in the visual row or next semantic row to the right is a placeholder.
// Focus should not move anywhere.
// Focus reached the end of the semantic row.
if (!flowHorizontally) {
// VERTICAL FLOW
// Check if the last semantic row in the LAST VISUAL row has been reached.
const rowsPerColumn = Math.ceil(rowsLength / numberOfColumns);
const lastVisualRowIndex = rowsPerColumn - 1;
const lastSemanticRowInLastVisualRow = getLastSemanticRowIndexInVisualRow(rowsLength, numberOfColumns, flowHorizontally, lastVisualRowIndex);
if (row === lastSemanticRowInLastVisualRow) {
// Focus should stay where it is and NOT move to the right.
return { row, cell };
}

// Check if the last semantic row in ANY VISUAL row has been reached.
const lastSemanticRowIndex = getLastSemanticRowIndexInVisualRow(rowsLength, numberOfColumns, flowHorizontally, row);
if (row === lastSemanticRowIndex) {
// Focus should wrap to the next next visual row.
const rowsToTop = row % rowsPerColumn;
return { row: rowsToTop + 1, cell: 0 };
}
}

if (flowHorizontally && row === rowsLength - 1) {
// HORIZONTAL FLOW
// The last row in the list has been reached, focus should not move to the right.
return { row, cell };
}
// Focus moves to the first cell of the next semantic row.

// Not the end of the visual row, focus moves to the first cell of the next semantic row.
nextCell = 0;
nextRow += flowHorizontally ? 1 : Math.ceil(rowsLength / numberOfColumns);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,29 +576,42 @@ describe('Compact Interactive List', () => {
list.simulate('keyDown', arrowRightProps);
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(11).instance());
// should not move to the right as the row end reached
// wrap to the beginning of the second visual row
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(11).instance());

// Move one row down to start testing left arrow
list.simulate('keyDown', arrowDownProps);
expect(document.activeElement).toBe(cellElements.at(3).instance());
// move to the end of the second visual row
list.simulate('keyDown', endKeyProps);
expect(document.activeElement).toBe(cellElements.at(14).instance());
// wrap to the beginning of the last (third) visual row
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(6).instance());
// move to the end of the last visual row
list.simulate('keyDown', endKeyProps);
expect(document.activeElement).toBe(cellElements.at(8).instance());
// stay at the end of the last visual row, as there is nowhere to wrap
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(8).instance());

// Testing LEFT ARROW
// move one cell to the left, same row
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(13).instance());
// move 2 cell to the left to break to the previous visual column
expect(document.activeElement).toBe(cellElements.at(7).instance());
// move 2 cell to the left to break to the previous visual row
list.simulate('keyDown', arrowLeftProps);
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(5).instance());
// move 2 cell to the left to reach the first visual column start
expect(document.activeElement).toBe(cellElements.at(14).instance());
// move 3 cell to the left to enter previous semantic column
list.simulate('keyDown', arrowLeftProps);
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(3).instance());
// should not move anymore as the start of the visual row has been reached
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(3).instance());
expect(document.activeElement).toBe(cellElements.at(5).instance());
// move up to reach the first row, then home to reach the first cell in first row
list.simulate('keyDown', arrowUpProps);
list.simulate('keyDown', homeKeyProps);
expect(document.activeElement).toBe(cellElements.at(0).instance());
// left arrow should not move focus anywhere
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(0).instance());
});

it('Up/Down Arrow should move through semantic column and break to the next/previous visual column once reached its start/end', () => {
Expand Down Expand Up @@ -801,29 +814,42 @@ describe('Compact Interactive List', () => {
list.simulate('keyDown', arrowRightProps);
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(5).instance());
// should not move to the right as the row end reached
// should move to the first cell of the next visual row
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(5).instance());

// Move one row down to start testing left arrow
list.simulate('keyDown', arrowDownProps);
expect(document.activeElement).toBe(cellElements.at(6).instance());
// move to the end of the visual row
list.simulate('keyDown', endKeyProps);
expect(document.activeElement).toBe(cellElements.at(11).instance());
// should move to the first cell of the next visual row
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(12).instance());
// move to the end of the visual row again
list.simulate('keyDown', endKeyProps);
expect(document.activeElement).toBe(cellElements.at(14).instance());
// should NOT move to the right from here as there is nowhere to move
list.simulate('keyDown', arrowRightProps);
expect(document.activeElement).toBe(cellElements.at(14).instance());

// Testing LEFT ARROW
// move one cell to the left, same row
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(10).instance());
// move 2 cell to the left to break to the previous visual column
expect(document.activeElement).toBe(cellElements.at(13).instance());
// move 2 cell to the left to break to the previous visual row
list.simulate('keyDown', arrowLeftProps);
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(8).instance());
// move 2 cell to the left to reach the first visual column start
expect(document.activeElement).toBe(cellElements.at(11).instance());
// move 3 cell to the left to break into previous semantic row
list.simulate('keyDown', arrowLeftProps);
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(6).instance());
// should not move anymore as the start of the visual row has been reached
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(6).instance());
expect(document.activeElement).toBe(cellElements.at(8).instance());
// move up to reach the first row, then home to reach the first cell in first row
list.simulate('keyDown', arrowUpProps);
list.simulate('keyDown', homeKeyProps);
expect(document.activeElement).toBe(cellElements.at(0).instance());
// left arrow should not move focus anywhere
list.simulate('keyDown', arrowLeftProps);
expect(document.activeElement).toBe(cellElements.at(0).instance());
});

it('Up/Down Arrow should move through semantic column and break to the next/previous visual column once reached its start/end', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/terra-framework-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Changed
* Updated `terra-compact-interactive-list` keyboard interactions descriptions for the left and right arrow keys.

* Added
* Added a note about accessibility requirements for sorting or another action to the Multiple Row Selection example in `terra-table`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ See the [Examples](/components/cerner-terra-framework-docs/compact-interactive-l
|---|---|
|UP ARROW | Selects the cell above the currently active cell. If the top cell of a column is active, the last cell of the previous column is selected. If the top cell of the first column is active, the active cell does not change.|
|DOWN ARROW | Selects the cell below the currently active cell. If the last cell of a column is active, the first cell of the next column is selected. If the last cell of the last column is active, the active cell does not change.|
|RIGHT ARROW | Selects the cell to the right of the currently active cell. If the right-most cell in the row is active, the active cell does not change.|
|LEFT ARROW | Selects the cell to the left of the currently active cell. If the left-most cell in the row is active, the active cell does not change.|
|RIGHT ARROW | Selects the cell to the right of the currently active cell. If the right-most cell in the visual row is active, the first cell in the next visual row will be selected.|
|LEFT ARROW | Selects the cell to the left of the currently active cell. If the left-most cell in the visual row is active, the last cell in the previous visual row will be selected.|
|HOME | Selects the first cell in the visual row.|
|END | Selects the last cell in the visual row.|
|CTRL+HOME (Microsoft Windows) <br/> or <br/> CTRL+COMMAND+LEFT ARROW (Apple Mac) | Selects the first cell in the first row.|
Expand Down

0 comments on commit 05f7d4f

Please sign in to comment.