Skip to content

Commit

Permalink
fix(datagrid): key navigation when using num pad (#1691)
Browse files Browse the repository at this point in the history
## PR Checklist

Please check if your PR fulfills the following requirements:

- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
- [ ] If applicable, have a visual design approval

## PR Type

What kind of change does this PR introduce?

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Other... Please describe:

## What is the current behavior?
Datagrid key navigation don't work with numpad keys when `NumLock` is off.


Issue Number: CDE-2564

## What is the new behavior?
Datagrid key navigation will work with numpad keys when `NumLock` is off.

## Does this PR introduce a breaking change?

- [ ] Yes
- [x] No

## Other information
  • Loading branch information
valentin-mladenov authored Jan 30, 2025
1 parent 70c3f25 commit ea844d0
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { animationFrameScheduler, BehaviorSubject, Observable } from 'rxjs';

import { ClarityModule } from '../../clr-angular.module';
import { Keys } from '../../utils/enums/keys.enum';
import { ClrDatagridVirtualScrollDirective } from './datagrid-virtual-scroll.directive';
import { DATAGRID_SPEC_PROVIDERS } from './helpers.spec';

Expand Down Expand Up @@ -221,18 +222,18 @@ export default function (): void {

expect(document.activeElement).toBe(headerCheckboxCell);

grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'PageDown' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.PageDown }));
// active checkbox input with ID clr-dg-row-cb364
expect(document.activeElement).toBe(grid.querySelectorAll('[type=checkbox]')[22]);

grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'PageDown' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.PageDown }));
sleep();
fixture.whenStable();
fixture.whenRenderingDone();
// active checkbox input with ID clr-dg-row-cb383
expect(document.activeElement).toBe(grid.querySelectorAll('[type=checkbox]')[41]);

grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'PageUp' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.PageUp }));
sleep();
fixture.whenStable();
fixture.whenRenderingDone();
Expand Down
52 changes: 33 additions & 19 deletions projects/angular/src/data/datagrid/datagrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,28 +897,42 @@ export default function (): void {
const grid = context.clarityElement.querySelector('[role=grid]');
expect(grid).toBeDefined();
const cells = grid.querySelectorAll('[role=gridcell], [role=columnheader]');
expect(cells.length).toBe(12); // 3*2 data, 3 select radios, 3 headers
expect(cells.length).toBe(12);
// data matrix 3*2 data, 3 select radios, 3 headers. Legend: 0h -> Index: 0, Type: header
// |0h| 1h| 2h|
// |3r| 4d| 5d|
// |6r| 7d| 8d|
// |9r|10d|11d|
// cell flow: start at index 0 -> 3 -> 4 (check) -> 5 (check) -> 8 (check)-> 7 (check)-> 4 (check) end

// need to start with this cell exactly, because it has tabindex=0
cells[0].focus();
expect(document.activeElement).toBe(cells[0]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowRight }));

grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowRight }));
expect(document.activeElement).toBe(cells[4]);

// second time, to avoid cycling over cells with radios
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowRight }));
expect(document.activeElement).toBe(cells[2]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowRight }));
expect(document.activeElement).toBe(cells[5]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowLeft }));

grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
expect(document.activeElement).toBe(cells[8]);

grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowLeft }));
expect(document.activeElement).toBe(cells[7]);

grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowUp }));
expect(document.activeElement).toBe(cells[4]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowUp }));
expect(document.activeElement).toBe(cells[1]);
});

it('Moves focus to inner actionable element', function () {
const grid = context.clarityElement.querySelector('[role=grid]');
const cells = grid.querySelectorAll('[role=gridcell], [role=columnheader]');
cells[0].focus();
expect(document.activeElement).toBe(cells[0]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
expect(document.activeElement).toBe(cells[3].querySelector('[type=radio]'));
});

Expand All @@ -927,12 +941,12 @@ export default function (): void {
const cells = grid.querySelectorAll('[role=gridcell], [role=columnheader]');
cells[0].focus();
expect(document.activeElement).toBe(cells[0]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
expect(document.activeElement).toBe(cells[9].querySelector('[type=radio]'));
// we're at the edge, then we click once more to get to the placeholder
grid.dispatchEvent(new KeyboardEvent('keydown', { code: Keys.ArrowDown }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.ArrowDown }));
expect(document.activeElement).toBe(cells[9].querySelector('[type=radio]'));
});

Expand All @@ -954,11 +968,11 @@ export default function (): void {
expect(document.activeElement).toBe(cells[0]);

// focus at bottom datagrid radio input
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'PageDown' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.PageDown }));
expect(document.activeElement).toBe(cells[9].querySelector('[type=radio]'));

// focus at top datagrid radio input
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'PageUp' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.PageUp }));
expect(document.activeElement).toBe(cells[3].querySelector('[type=radio]'));
});

Expand All @@ -967,9 +981,9 @@ export default function (): void {
const cells = grid.querySelectorAll('[role=gridcell], [role=columnheader]');
cells[0].focus();
expect(document.activeElement).toBe(cells[0]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'End' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.End }));
expect(document.activeElement).toBe(cells[2]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'Home' }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.Home }));
expect(document.activeElement).toBe(cells[0]);
});

Expand All @@ -978,9 +992,9 @@ export default function (): void {
const cells = grid.querySelectorAll('[role=gridcell], [role=columnheader]');
cells[0].focus();
expect(document.activeElement).toBe(cells[0]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'End', ctrlKey: true }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.End, ctrlKey: true }));
expect(document.activeElement).toBe(cells[11]);
grid.dispatchEvent(new KeyboardEvent('keydown', { code: 'Home', ctrlKey: true }));
grid.dispatchEvent(new KeyboardEvent('keydown', { key: Keys.Home, ctrlKey: true }));
expect(document.activeElement).toBe(cells[0]);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { Keys } from '../../../utils/enums/keys.enum';

export function getTabableItems(el: HTMLElement) {
const tabableSelector = [
'a[href]',
Expand Down Expand Up @@ -103,19 +105,19 @@ export class KeyNavigationGridController implements OnDestroy {
// Skip column resize events
if (
(e.target as HTMLElement).classList.contains('drag-handle') &&
(e.code === 'ArrowLeft' || e.code === 'ArrowRight')
(e.key === Keys.ArrowLeft || e.key === Keys.ArrowRight)
) {
return;
}
if (
e.code === 'ArrowUp' ||
e.code === 'ArrowDown' ||
e.code === 'ArrowLeft' ||
e.code === 'ArrowRight' ||
e.code === 'End' ||
e.code === 'Home' ||
e.code === 'PageUp' ||
e.code === 'PageDown'
e.key === Keys.ArrowUp ||
e.key === Keys.ArrowDown ||
e.key === Keys.ArrowLeft ||
e.key === Keys.ArrowRight ||
e.key === Keys.End ||
e.key === Keys.Home ||
e.key === Keys.PageUp ||
e.key === Keys.PageDown
) {
const { x, y } = this.getNextItemCoordinate(e);
const activeItem = this.rows
Expand Down Expand Up @@ -171,7 +173,7 @@ export class KeyNavigationGridController implements OnDestroy {

private getNextItemCoordinate(e: any) {
let currentCell = this.cells ? Array.from(this.cells).find(i => i.getAttribute('tabindex') === '0') : null;
if (e.code === 'Tab') {
if (e.key === Keys.Tab) {
currentCell = document.activeElement as HTMLElement;
}
const currentRow = this.rows && currentCell ? Array.from(this.rows).find(r => r.contains(currentCell)) : null;
Expand All @@ -185,35 +187,35 @@ export class KeyNavigationGridController implements OnDestroy {
let y = currentRow && currentCell && this.rows ? Array.from(this.rows).indexOf(currentRow) : 0;

const dir = this.host.dir;
const inlineStart = dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
const inlineEnd = dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
const inlineStart = dir === 'rtl' ? Keys.ArrowRight : Keys.ArrowLeft;
const inlineEnd = dir === 'rtl' ? Keys.ArrowLeft : Keys.ArrowRight;

const itemsPerPage =
Math.floor(this.host?.querySelector('.datagrid').clientHeight / this.rows[0].clientHeight) - 1 || 0;

if (e.code === 'ArrowUp' && y !== 0) {
if (e.key === Keys.ArrowUp && y !== 0) {
y = y - 1;
} else if (e.code === 'ArrowDown' && y < numOfRows) {
} else if (e.key === Keys.ArrowDown && y < numOfRows) {
y = y + 1;
} else if (e.code === inlineStart && x !== 0) {
} else if (e.key === inlineStart && x !== 0) {
x = x - 1;
} else if (e.code === inlineEnd && x < numOfColumns) {
} else if (e.key === inlineEnd && x < numOfColumns) {
x = x + 1;
} else if (e.code === 'End') {
} else if (e.key === Keys.End) {
x = numOfColumns;

if (e.ctrlKey) {
y = numOfRows;
}
} else if (e.code === 'Home') {
} else if (e.key === Keys.Home) {
x = 0;

if (e.ctrlKey) {
y = 0;
}
} else if (e.code === 'PageUp') {
} else if (e.key === Keys.PageUp) {
y = y - itemsPerPage > 0 ? y - itemsPerPage + 1 : 1;
} else if (e.code === 'PageDown') {
} else if (e.key === Keys.PageDown) {
y = y + itemsPerPage < numOfRows ? y + itemsPerPage : numOfRows;
}

Expand Down
2 changes: 2 additions & 0 deletions projects/angular/src/utils/enums/keys.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export enum Keys {
Spacebar = ' ',
Home = 'Home',
End = 'End',
PageDown = 'PageDown',
PageUp = 'PageUp',
}

export enum IEKeys {
Expand Down

0 comments on commit ea844d0

Please sign in to comment.