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

Allow break to the next/previous visual row on right/left arrow key. #2146

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Changes from 1 commit
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
Next Next commit
right/left arrow keyboard nav wraps to the next/previous visual row
adoroshk committed Apr 24, 2024
commit 5cd3097f5d35ea7c6b2410af34f13c1c5029ba8b
3 changes: 3 additions & 0 deletions packages/terra-compact-interactive-list/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

## Unreleased

* Changed
* Key board navigation updated to wrap to the next/previous line at the end/start of a visual row.
adoroshk marked this conversation as resolved.
Show resolved Hide resolved

## 1.20.0 - (April 23, 2024)

* Changed
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
@@ -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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality wasn't added in this change, it existed before. I just had to move it up in order to use in another method.

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.
@@ -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);
@@ -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.
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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', () => {
@@ -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', () => {
3 changes: 3 additions & 0 deletions packages/terra-framework-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

## Unreleased

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

## 1.84.0 - (April 23, 2024)

* Changed
Original file line number Diff line number Diff line change
@@ -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.|