diff --git a/packages/terra-compact-interactive-list/CHANGELOG.md b/packages/terra-compact-interactive-list/CHANGELOG.md index b3a90328451..a230c3b2c90 100644 --- a/packages/terra-compact-interactive-list/CHANGELOG.md +++ b/packages/terra-compact-interactive-list/CHANGELOG.md @@ -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 diff --git a/packages/terra-compact-interactive-list/src/utils/keyHandlerUtils.js b/packages/terra-compact-interactive-list/src/utils/keyHandlerUtils.js index 5b7eb1fffc2..c7a97758992 100644 --- a/packages/terra-compact-interactive-list/src/utils/keyHandlerUtils.js +++ b/packages/terra-compact-interactive-list/src/utils/keyHandlerUtils.js @@ -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. @@ -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 { diff --git a/packages/terra-compact-interactive-list/tests/jest/CompactInteractiveList.test.jsx b/packages/terra-compact-interactive-list/tests/jest/CompactInteractiveList.test.jsx index 1ce5a1f022a..fbcd749c1db 100644 --- a/packages/terra-compact-interactive-list/tests/jest/CompactInteractiveList.test.jsx +++ b/packages/terra-compact-interactive-list/tests/jest/CompactInteractiveList.test.jsx @@ -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', () => { diff --git a/packages/terra-framework-docs/CHANGELOG.md b/packages/terra-framework-docs/CHANGELOG.md index 562da5f2f37..64ad96bd1df 100644 --- a/packages/terra-framework-docs/CHANGELOG.md +++ b/packages/terra-framework-docs/CHANGELOG.md @@ -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`. diff --git a/packages/terra-framework-docs/src/terra-dev-site/doc/compact-interactive-list/About.1.doc.mdx b/packages/terra-framework-docs/src/terra-dev-site/doc/compact-interactive-list/About.1.doc.mdx index a451c55112e..bd262ae5115 100644 --- a/packages/terra-framework-docs/src/terra-dev-site/doc/compact-interactive-list/About.1.doc.mdx +++ b/packages/terra-framework-docs/src/terra-dev-site/doc/compact-interactive-list/About.1.doc.mdx @@ -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)
or
CTRL+COMMAND+LEFT ARROW (Apple Mac) | Selects the first cell in the first row.|