Skip to content

Commit

Permalink
cell keyboard navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot committed Jul 28, 2023
1 parent f5745ca commit 3d2956a
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 8 deletions.
77 changes: 69 additions & 8 deletions src/table/grid-navigation/use-grid-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { useEffect, useMemo } from 'react';
import { findFocusinCell } from './utils';
import { findFocusinCell, moveFocusBy } from './utils';
import { FocusedCell, GridNavigationAPI, GridNavigationProps } from './interfaces';
import { KeyCode } from '../../internal/keycode';

export function useGridNavigation({ tableRole, pageSize, getContainer }: GridNavigationProps): GridNavigationAPI {
const model = useMemo(() => new GridNavigationModel(), []);
Expand All @@ -22,9 +23,7 @@ export function useGridNavigation({ tableRole, pageSize, getContainer }: GridNav
[model, tableRole]
);

// TODO: is columns/rows actually needed??
// TODO: handle the case when rows and columns stay unchanged by the focused item disappears (e.g. it is replaced by another item)
// Notify the model of the props change. The focus might need to move when the focused cell is no longer available.
// Notify the model of the props change.
useEffect(() => {
model.update({ pageSize });
}, [model, pageSize]);
Expand Down Expand Up @@ -80,15 +79,77 @@ class GridNavigationModel {
return;
}
this.focusedCell = cell;

console.log('FOCUS IN', cell.rowIndex, cell.colIndex, cell.element);
};

private onFocusout = () => {
this.focusedCell = null;
};

private onKeydown = () => {
console.log('onkeydown');
private onKeydown = (event: KeyboardEvent) => {
if (!this.focusedCell) {
return;
}

const ctrlKey = event.ctrlKey ? 1 : 0;
const altKey = event.altKey ? 1 : 0;
const shiftKey = event.shiftKey ? 1 : 0;
const metaKey = event.metaKey ? 1 : 0;
const numModifiersPressed = ctrlKey + altKey + shiftKey + metaKey;

let key = event.keyCode;
if (numModifiersPressed === 1 && event.ctrlKey) {
key = -key;
} else if (numModifiersPressed) {
return;
}

const from = this.focusedCell;
const minExtreme = Number.NEGATIVE_INFINITY;
const maxExtreme = Number.POSITIVE_INFINITY;

switch (key) {
case KeyCode.up:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: -1, colIndex: 0 });

case KeyCode.down:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: 1, colIndex: 0 });

case KeyCode.left:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: 0, colIndex: -1 });

case KeyCode.right:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: 0, colIndex: 1 });

case KeyCode.pageUp:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: -this.pageSize, colIndex: 0 });

case KeyCode.pageDown:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: this.pageSize, colIndex: 0 });

case KeyCode.home:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: 0, colIndex: minExtreme });

case KeyCode.end:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: 0, colIndex: maxExtreme });

case -KeyCode.home:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: minExtreme, colIndex: minExtreme });

case -KeyCode.end:
event.preventDefault();
return moveFocusBy(this.container, from, { rowIndex: maxExtreme, colIndex: maxExtreme });

default:
return;
}
};
}
46 changes: 46 additions & 0 deletions src/table/grid-navigation/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { getFirstFocusable } from '../../internal/components/focus-lock/utils';
import { FocusedCell } from './interfaces';

export function findFocusinCell(event: FocusEvent): null | FocusedCell {
Expand All @@ -23,3 +24,48 @@ export function findFocusinCell(event: FocusEvent): null | FocusedCell {

return { rowIndex: isNaN(rowIndex) ? 0 : rowIndex, colIndex, cellElement: closestCell, element: event.target };
}

export function moveFocusBy(
container: HTMLElement,
from: { rowIndex: number; colIndex: number },
delta: { rowIndex: number; colIndex: number }
) {
const targetRowIndex = from.rowIndex + delta.rowIndex;
const targetColIndex = from.colIndex + delta.colIndex;

let targetRow: null | HTMLTableRowElement = null;
let targetCell: null | HTMLTableCellElement = null;

const rowElements = container.querySelectorAll('tr[aria-rowindex]');
for (let elementIndex = 0; elementIndex < rowElements.length; elementIndex++) {
const rowIndex = parseInt(rowElements[elementIndex].getAttribute('aria-rowindex') ?? '');
targetRow = rowElements[elementIndex] as HTMLTableRowElement;

if (rowIndex === targetRowIndex || (delta.rowIndex < 0 && rowIndex >= targetRowIndex)) {
break;
}
}
if (!targetRow) {
return;
}

const cellElements = targetRow.querySelectorAll('td[aria-colindex],th[aria-colindex]');
for (let elementIndex = 0; elementIndex < cellElements.length; elementIndex++) {
const columnIndex = parseInt(cellElements[elementIndex].getAttribute('aria-colindex') ?? '');
targetCell = cellElements[elementIndex] as HTMLTableCellElement;

if (columnIndex === targetColIndex || (delta.colIndex < 0 && columnIndex >= targetColIndex)) {
break;
}
}
if (!targetCell) {
return;
}

const firstFocusableInsideCell = getFirstFocusable(targetCell);
if (firstFocusableInsideCell) {
firstFocusableInsideCell.focus();
} else {
targetCell.focus();
}
}

0 comments on commit 3d2956a

Please sign in to comment.