From 934765f2f6aadf0bf3ee05a9053b930bda9b032e Mon Sep 17 00:00:00 2001
From: Dominik Iwanek <141320833+dominikiwanekhyland@users.noreply.github.com>
Date: Wed, 21 Jan 2026 12:49:47 +0100
Subject: [PATCH] [ACS-10304] KN: personal files: Column resizing available via
mouse but not accessible via keyboard (#11525)
---
.../datatable/datatable.component.html | 29 ++--
.../datatable/datatable.component.scss | 21 ++-
.../resizable/resizable.directive.spec.ts | 8 +
.../resizable/resizable.directive.ts | 13 ++
.../resizable/resize-handle.directive.spec.ts | 159 +++++++++++++++---
.../resizable/resize-handle.directive.ts | 32 +++-
lib/core/src/lib/i18n/en.json | 4 +-
7 files changed, 226 insertions(+), 40 deletions(-)
diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.html b/lib/core/src/lib/datatable/components/datatable/datatable.component.html
index 0903483996..0a495a703a 100644
--- a/lib/core/src/lib/datatable/components/datatable/datatable.component.html
+++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.html
@@ -76,6 +76,7 @@
#resizableElement="adf-resizable"
[coverPadding]="10"
(resizing)="onResizing($event, columnIndex)"
+ (keyboardResizing)="onResizing($event, columnIndex)"
(resizeStart)="resizingColumnIndex = columnIndex"
(resizeEnd)="onResizingEnd()"
[attr.data-automation-id]="'auto_header_content_id_' + col.key"
@@ -134,18 +135,22 @@
/>
-
+
diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.scss b/lib/core/src/lib/datatable/components/datatable/datatable.component.scss
index 585a16d565..37aa44d062 100644
--- a/lib/core/src/lib/datatable/components/datatable/datatable.component.scss
+++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.scss
@@ -27,13 +27,21 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
&__resize-handle {
padding: 0 2px;
+ position: relative;
&-visible {
visibility: visible;
+ opacity: 1;
}
&-hidden {
- visibility: hidden;
+ opacity: 0;
+
+ &:focus,
+ &:focus-visible,
+ &:focus-within {
+ opacity: 1;
+ }
}
&--divider {
@@ -44,6 +52,17 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
&:hover {
cursor: col-resize;
}
+
+ &:focus,
+ &:focus-visible {
+ opacity: 1;
+ outline: 2px solid var(--theme-accent-color-a200);
+ outline-offset: -1px;
+
+ .adf-datatable__resize-handle--divider {
+ border-color: var(--theme-accent-color-a200);
+ }
+ }
}
&__cursor--pointer {
diff --git a/lib/core/src/lib/datatable/directives/resizable/resizable.directive.spec.ts b/lib/core/src/lib/datatable/directives/resizable/resizable.directive.spec.ts
index dfbc2f334b..f0bb7d910a 100644
--- a/lib/core/src/lib/datatable/directives/resizable/resizable.directive.spec.ts
+++ b/lib/core/src/lib/datatable/directives/resizable/resizable.directive.spec.ts
@@ -158,4 +158,12 @@ describe('ResizableDirective', () => {
expect(directive.resizing.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, bottom: 0, right: 130, width: 130 } });
});
+
+ it('should emit keyboardResizing event when resizeByKeyboard', () => {
+ spyOn(directive.keyboardResizing, 'emit');
+ const step = 20;
+ directive.resizeByKeyboard(step);
+
+ expect(directive.keyboardResizing.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, bottom: 0, right: step, width: step } });
+ });
});
diff --git a/lib/core/src/lib/datatable/directives/resizable/resizable.directive.ts b/lib/core/src/lib/datatable/directives/resizable/resizable.directive.ts
index 6bc2e370ce..712e7b41e0 100644
--- a/lib/core/src/lib/datatable/directives/resizable/resizable.directive.ts
+++ b/lib/core/src/lib/datatable/directives/resizable/resizable.directive.ts
@@ -41,6 +41,11 @@ export class ResizableDirective implements OnInit, OnDestroy {
*/
@Output() resizeEnd = new EventEmitter();
+ /**
+ * Emitted when keyboard resize is triggered.
+ */
+ @Output() keyboardResizing = new EventEmitter();
+
/**
* This is to cover sum of the left and right padding between resize handler and its parent.
*/
@@ -174,6 +179,14 @@ export class ResizableDirective implements OnInit, OnDestroy {
this.unsubscribeMouseUp?.();
}
+ resizeByKeyboard(delta: number): void {
+ const currentRect = this.getElementRect(this.element);
+ const rectangle = this.getNewBoundingRectangle(currentRect, delta);
+ this.zone.run(() => {
+ this.keyboardResizing.emit({ rectangle });
+ });
+ }
+
private getNewBoundingRectangle({ top, bottom, left, right }: BoundingRectangle, clientX: number): BoundingRectangle {
const updatedRight = Math.round(right + clientX);
diff --git a/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.spec.ts b/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.spec.ts
index 576b545ae4..16122149f0 100644
--- a/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.spec.ts
+++ b/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.spec.ts
@@ -15,42 +15,151 @@
* limitations under the License.
*/
-import { TestBed } from '@angular/core/testing';
import { ElementRef, NgZone, Renderer2 } from '@angular/core';
import { ResizeHandleDirective } from './resize-handle.directive';
+import { ResizableDirective } from './resizable.directive';
+import { Subject } from 'rxjs';
describe('ResizeHandleDirective', () => {
- let ngZone: NgZone;
- let renderer: Renderer2;
- let element: ElementRef;
let directive: ResizeHandleDirective;
-
- const rendererMock = {
- listen: jasmine.createSpy('listen')
- };
-
- const elementRefMock = {
- nativeElement: { dispatchEvent: () => {} }
- };
+ let renderer: jasmine.SpyObj;
+ let element: ElementRef;
+ let ngZone: NgZone;
+ let resizableContainer: jasmine.SpyObj;
beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [ResizeHandleDirective],
- providers: [
- { provide: Renderer2, useValue: rendererMock },
- { provide: ElementRef, useValue: elementRefMock }
- ]
+ renderer = jasmine.createSpyObj('Renderer2', ['listen']);
+ element = { nativeElement: document.createElement('div') };
+ ngZone = { runOutsideAngular: (fn: () => void) => fn() } as NgZone;
+
+ resizableContainer = jasmine.createSpyObj('ResizableDirective', ['resizeByKeyboard'], {
+ mousedown: new Subject(),
+ mouseup: new Subject(),
+ mousemove: new Subject()
});
- element = TestBed.inject(ElementRef);
- renderer = TestBed.inject(Renderer2);
- ngZone = TestBed.inject(NgZone);
- spyOn(ngZone, 'runOutsideAngular').and.callFake((fn) => fn());
directive = new ResizeHandleDirective(renderer, element, ngZone);
- directive.ngOnInit();
+ directive.resizableContainer = resizableContainer;
});
- it('should attach mousedown event on resizable element', () => {
- expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousedown', jasmine.any(Function));
+ describe('ngOnInit', () => {
+ it('should attach mousedown event listener on element', () => {
+ renderer.listen.and.returnValue(() => {});
+
+ directive.ngOnInit();
+
+ expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousedown', jasmine.any(Function));
+ });
+
+ it('should run mousedown listener outside Angular zone', () => {
+ const runOutsideAngularSpy = spyOn(ngZone, 'runOutsideAngular').and.callThrough();
+ renderer.listen.and.returnValue(() => {});
+
+ directive.ngOnInit();
+
+ expect(runOutsideAngularSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('ngOnDestroy', () => {
+ it('should unsubscribe from mousedown listener', () => {
+ const unlistenMouseDown = jasmine.createSpy('unlistenMouseDown');
+ renderer.listen.and.returnValue(unlistenMouseDown);
+
+ directive.ngOnInit();
+ directive.ngOnDestroy();
+
+ expect(unlistenMouseDown).toHaveBeenCalled();
+ });
+ });
+
+ describe('mouse events', () => {
+ let mousedownCallback: (event: MouseEvent) => void;
+
+ beforeEach(() => {
+ renderer.listen.and.callFake((_target: any, eventName: string, callback: (event: MouseEvent) => void) => {
+ if (eventName === 'mousedown') {
+ mousedownCallback = callback;
+ }
+ return () => {};
+ });
+ directive.ngOnInit();
+ });
+
+ it('should emit mousedown event to resizable container with resize flag', () => {
+ spyOn(resizableContainer.mousedown, 'next');
+ const mouseEvent = new MouseEvent('mousedown', { cancelable: true });
+
+ mousedownCallback(mouseEvent);
+
+ expect(resizableContainer.mousedown.next).toHaveBeenCalledWith(jasmine.objectContaining({ resize: true }));
+ });
+
+ it('should prevent default on cancelable mousedown event', () => {
+ const mouseEvent = new MouseEvent('mousedown', { cancelable: true });
+ spyOn(mouseEvent, 'preventDefault');
+
+ mousedownCallback(mouseEvent);
+
+ expect(mouseEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should not prevent default on non-cancelable mousedown event', () => {
+ const mouseEvent = new MouseEvent('mousedown', { cancelable: false });
+ spyOn(mouseEvent, 'preventDefault');
+
+ mousedownCallback(mouseEvent);
+
+ expect(mouseEvent.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('should attach mousemove and mouseup listeners on mousedown', () => {
+ const mouseEvent = new MouseEvent('mousedown', { cancelable: true });
+
+ mousedownCallback(mouseEvent);
+
+ expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousemove', jasmine.any(Function));
+ expect(renderer.listen).toHaveBeenCalledWith('document', 'mouseup', jasmine.any(Function));
+ });
+ });
+
+ describe('keyboard resizing', () => {
+ it('should call resizeByKeyboard with positive delta on ArrowRight', () => {
+ const event = new KeyboardEvent('keydown', { key: 'ArrowRight' });
+ spyOn(event, 'preventDefault');
+ spyOn(event, 'stopPropagation');
+
+ directive.onKeydown(event);
+
+ expect(resizableContainer.resizeByKeyboard).toHaveBeenCalledWith(20);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(event.stopPropagation).toHaveBeenCalled();
+ });
+
+ it('should use larger step with Shift+ArrowRight', () => {
+ const event = new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true });
+
+ directive.onKeydown(event);
+
+ expect(resizableContainer.resizeByKeyboard).toHaveBeenCalledWith(60);
+ });
+
+ it('should call resizeByKeyboard with negative delta on Shift+ArrowLeft', () => {
+ const event = new KeyboardEvent('keydown', { key: 'ArrowLeft', shiftKey: true });
+
+ directive.onKeydown(event);
+
+ expect(resizableContainer.resizeByKeyboard).toHaveBeenCalledWith(-40);
+ });
+
+ it('should not call resizeByKeyboard for unrelated keys', () => {
+ const event = new KeyboardEvent('keydown', { key: 'Enter' });
+ spyOn(event, 'preventDefault');
+
+ directive.onKeydown(event);
+
+ expect(resizableContainer.resizeByKeyboard).not.toHaveBeenCalled();
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ });
});
});
diff --git a/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.ts b/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.ts
index 0613154b3c..a827a05851 100644
--- a/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.ts
+++ b/lib/core/src/lib/datatable/directives/resizable/resize-handle.directive.ts
@@ -16,7 +16,7 @@
*/
import { ResizableDirective } from './resizable.directive';
-import { Directive, ElementRef, Input, NgZone, OnDestroy, OnInit, Renderer2 } from '@angular/core';
+import { Directive, ElementRef, HostListener, Input, NgZone, OnDestroy, OnInit, Renderer2 } from '@angular/core';
@Directive({
selector: '[adf-resize-handle]'
@@ -32,6 +32,35 @@ export class ResizeHandleDirective implements OnInit, OnDestroy {
private unlistenMouseUp?: () => void;
constructor(private readonly renderer: Renderer2, private readonly element: ElementRef, private readonly zone: NgZone) {}
+ @HostListener('keydown', ['$event'])
+ onKeydown(event: KeyboardEvent): void {
+ let delta: number | null = null;
+ const shiftDelta = 40;
+ if (event.shiftKey) {
+ delta += shiftDelta;
+ }
+ const rightStepBaseValue = 20;
+ switch (event.key) {
+ case 'ArrowRight':
+ case 'ArrowUp':
+ delta += rightStepBaseValue;
+ break;
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ delta = -delta;
+ break;
+ default:
+ break;
+ }
+
+ if (delta !== null) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.resizableContainer.resizeByKeyboard(delta);
+ }
+ }
+
ngOnInit(): void {
this.zone.runOutsideAngular(() => {
this.unlistenMouseDown = this.renderer.listen(this.element.nativeElement, 'mousedown', (mouseDownEvent: MouseEvent) => {
@@ -66,6 +95,7 @@ export class ResizeHandleDirective implements OnInit, OnDestroy {
private onMouseup(event: MouseEvent): void {
this.unlistenMouseMove?.();
+ this.unlistenMouseMove = undefined;
this.unlistenMouseUp();
this.resizableContainer.mouseup.next(event);
}
diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json
index 98bbed25a8..5c1ad1a1ad 100644
--- a/lib/core/src/lib/i18n/en.json
+++ b/lib/core/src/lib/i18n/en.json
@@ -406,7 +406,9 @@
"ICON_DISABLED": "Disabled",
"ROW_OPTION_BUTTON": "Actions",
"DRAG": "Drag button",
- "EMPTY_HEADER": "Empty header"
+ "EMPTY_HEADER": "Empty header",
+ "RESIZE_COLUMN": "Resize {{ column }} column. Use arrow keys to adjust width, Shift for larger steps.",
+ "COLUMN_WIDTH_CHANGED": "{{ column }} column width: {{ width }} pixels"
},
"FILE_TYPE": {
"DOCUMENT": "Document",