From ea844d079f0dd6553367126d3710af1bb92cd637 Mon Sep 17 00:00:00 2001
From: Valentin Mladenov <valentin.mladenov@broadcom.com>
Date: Thu, 30 Jan 2025 17:58:16 +0200
Subject: [PATCH] fix(datagrid): key navigation when using num pad (#1691)

## 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
---
 .../datagrid-virtual-scroll.directive.spec.ts |  7 +--
 .../src/data/datagrid/datagrid.spec.ts        | 52 ++++++++++++-------
 .../utils/key-navigation-grid.controller.ts   | 42 ++++++++-------
 projects/angular/src/utils/enums/keys.enum.ts |  2 +
 4 files changed, 61 insertions(+), 42 deletions(-)

diff --git a/projects/angular/src/data/datagrid/datagrid-virtual-scroll.directive.spec.ts b/projects/angular/src/data/datagrid/datagrid-virtual-scroll.directive.spec.ts
index 002616e5a1..03c739c22c 100644
--- a/projects/angular/src/data/datagrid/datagrid-virtual-scroll.directive.spec.ts
+++ b/projects/angular/src/data/datagrid/datagrid-virtual-scroll.directive.spec.ts
@@ -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';
 
@@ -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();
diff --git a/projects/angular/src/data/datagrid/datagrid.spec.ts b/projects/angular/src/data/datagrid/datagrid.spec.ts
index 03d2d8618d..0df8d32a93 100644
--- a/projects/angular/src/data/datagrid/datagrid.spec.ts
+++ b/projects/angular/src/data/datagrid/datagrid.spec.ts
@@ -897,20 +897,34 @@ 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 () {
@@ -918,7 +932,7 @@ 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', { key: Keys.ArrowDown }));
         expect(document.activeElement).toBe(cells[3].querySelector('[type=radio]'));
       });
 
@@ -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]'));
       });
 
@@ -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]'));
       });
 
@@ -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]);
       });
 
@@ -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]);
       });
     });
diff --git a/projects/angular/src/data/datagrid/utils/key-navigation-grid.controller.ts b/projects/angular/src/data/datagrid/utils/key-navigation-grid.controller.ts
index 4cf5ec3960..9a6411ec63 100644
--- a/projects/angular/src/data/datagrid/utils/key-navigation-grid.controller.ts
+++ b/projects/angular/src/data/datagrid/utils/key-navigation-grid.controller.ts
@@ -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]',
@@ -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
@@ -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;
@@ -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;
     }
 
diff --git a/projects/angular/src/utils/enums/keys.enum.ts b/projects/angular/src/utils/enums/keys.enum.ts
index 39ae8f68f3..df24efccc0 100644
--- a/projects/angular/src/utils/enums/keys.enum.ts
+++ b/projects/angular/src/utils/enums/keys.enum.ts
@@ -18,6 +18,8 @@ export enum Keys {
   Spacebar = ' ',
   Home = 'Home',
   End = 'End',
+  PageDown = 'PageDown',
+  PageUp = 'PageUp',
 }
 
 export enum IEKeys {