mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[ACS-10304] KN: personal files: Column resizing available via mouse but not accessible via keyboard (#11525)
This commit is contained in:
@@ -76,6 +76,7 @@
|
|||||||
#resizableElement="adf-resizable"
|
#resizableElement="adf-resizable"
|
||||||
[coverPadding]="10"
|
[coverPadding]="10"
|
||||||
(resizing)="onResizing($event, columnIndex)"
|
(resizing)="onResizing($event, columnIndex)"
|
||||||
|
(keyboardResizing)="onResizing($event, columnIndex)"
|
||||||
(resizeStart)="resizingColumnIndex = columnIndex"
|
(resizeStart)="resizingColumnIndex = columnIndex"
|
||||||
(resizeEnd)="onResizingEnd()"
|
(resizeEnd)="onResizingEnd()"
|
||||||
[attr.data-automation-id]="'auto_header_content_id_' + col.key"
|
[attr.data-automation-id]="'auto_header_content_id_' + col.key"
|
||||||
@@ -134,18 +135,22 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="isResizingEnabled && col.resizable && !lastColumn"
|
*ngIf="isResizingEnabled && col.resizable && !lastColumn"
|
||||||
[ngClass]="hoveredHeaderColumnIndex === columnIndex && !isResizing || resizingColumnIndex === columnIndex ? 'adf-datatable__resize-handle-visible' : 'adf-datatable__resize-handle-hidden'"
|
[ngClass]="hoveredHeaderColumnIndex === columnIndex && !isResizing || resizingColumnIndex === columnIndex ? 'adf-datatable__resize-handle-visible' : 'adf-datatable__resize-handle-hidden'"
|
||||||
adf-resize-handle
|
adf-resize-handle
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="slider"
|
||||||
(click)="$event.stopPropagation()"
|
[attr.aria-valuenow]="col.width ?? 100"
|
||||||
(keyup.enter)="$event.stopPropagation()"
|
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.RESIZE_COLUMN' | translate: { column: col.title | translate }"
|
||||||
class="adf-datatable__resize-handle"
|
[attr.aria-valuemin]="100"
|
||||||
[resizableContainer]="resizableElement">
|
[attr.aria-valuemax]="500"
|
||||||
<div class="adf-datatable__resize-handle--divider"></div>
|
(click)="$event.stopPropagation()"
|
||||||
</div>
|
(keydown)="$event.stopPropagation()"
|
||||||
|
class="adf-datatable__resize-handle"
|
||||||
|
[resizableContainer]="resizableElement">
|
||||||
|
<div class="adf-datatable__resize-handle--divider"></div>
|
||||||
|
</div>
|
||||||
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
|
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -27,13 +27,21 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
|
|||||||
|
|
||||||
&__resize-handle {
|
&__resize-handle {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&-visible {
|
&-visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-hidden {
|
&-hidden {
|
||||||
visibility: hidden;
|
opacity: 0;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible,
|
||||||
|
&:focus-within {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--divider {
|
&--divider {
|
||||||
@@ -44,6 +52,17 @@ $data-table-cell-min-width-file-size: $data-table-cell-min-width-1 !default;
|
|||||||
&:hover {
|
&:hover {
|
||||||
cursor: col-resize;
|
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 {
|
&__cursor--pointer {
|
||||||
|
|||||||
@@ -158,4 +158,12 @@ describe('ResizableDirective', () => {
|
|||||||
|
|
||||||
expect(directive.resizing.emit).toHaveBeenCalledWith({ rectangle: { top: 0, left: 0, bottom: 0, right: 130, width: 130 } });
|
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 } });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export class ResizableDirective implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() resizeEnd = new EventEmitter<ResizeEvent>();
|
@Output() resizeEnd = new EventEmitter<ResizeEvent>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when keyboard resize is triggered.
|
||||||
|
*/
|
||||||
|
@Output() keyboardResizing = new EventEmitter<ResizeEvent>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is to cover sum of the left and right padding between resize handler and its parent.
|
* 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?.();
|
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 {
|
private getNewBoundingRectangle({ top, bottom, left, right }: BoundingRectangle, clientX: number): BoundingRectangle {
|
||||||
const updatedRight = Math.round(right + clientX);
|
const updatedRight = Math.round(right + clientX);
|
||||||
|
|
||||||
|
|||||||
@@ -15,42 +15,151 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { ElementRef, NgZone, Renderer2 } from '@angular/core';
|
import { ElementRef, NgZone, Renderer2 } from '@angular/core';
|
||||||
import { ResizeHandleDirective } from './resize-handle.directive';
|
import { ResizeHandleDirective } from './resize-handle.directive';
|
||||||
|
import { ResizableDirective } from './resizable.directive';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
describe('ResizeHandleDirective', () => {
|
describe('ResizeHandleDirective', () => {
|
||||||
let ngZone: NgZone;
|
|
||||||
let renderer: Renderer2;
|
|
||||||
let element: ElementRef;
|
|
||||||
let directive: ResizeHandleDirective;
|
let directive: ResizeHandleDirective;
|
||||||
|
let renderer: jasmine.SpyObj<Renderer2>;
|
||||||
const rendererMock = {
|
let element: ElementRef;
|
||||||
listen: jasmine.createSpy('listen')
|
let ngZone: NgZone;
|
||||||
};
|
let resizableContainer: jasmine.SpyObj<ResizableDirective>;
|
||||||
|
|
||||||
const elementRefMock = {
|
|
||||||
nativeElement: { dispatchEvent: () => {} }
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
renderer = jasmine.createSpyObj('Renderer2', ['listen']);
|
||||||
imports: [ResizeHandleDirective],
|
element = { nativeElement: document.createElement('div') };
|
||||||
providers: [
|
ngZone = { runOutsideAngular: (fn: () => void) => fn() } as NgZone;
|
||||||
{ provide: Renderer2, useValue: rendererMock },
|
|
||||||
{ provide: ElementRef, useValue: elementRefMock }
|
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 = new ResizeHandleDirective(renderer, element, ngZone);
|
||||||
directive.ngOnInit();
|
directive.resizableContainer = resizableContainer;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should attach mousedown event on resizable element', () => {
|
describe('ngOnInit', () => {
|
||||||
expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousedown', jasmine.any(Function));
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ResizableDirective } from './resizable.directive';
|
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({
|
@Directive({
|
||||||
selector: '[adf-resize-handle]'
|
selector: '[adf-resize-handle]'
|
||||||
@@ -32,6 +32,35 @@ export class ResizeHandleDirective implements OnInit, OnDestroy {
|
|||||||
private unlistenMouseUp?: () => void;
|
private unlistenMouseUp?: () => void;
|
||||||
constructor(private readonly renderer: Renderer2, private readonly element: ElementRef, private readonly zone: NgZone) {}
|
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 {
|
ngOnInit(): void {
|
||||||
this.zone.runOutsideAngular(() => {
|
this.zone.runOutsideAngular(() => {
|
||||||
this.unlistenMouseDown = this.renderer.listen(this.element.nativeElement, 'mousedown', (mouseDownEvent: MouseEvent) => {
|
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 {
|
private onMouseup(event: MouseEvent): void {
|
||||||
this.unlistenMouseMove?.();
|
this.unlistenMouseMove?.();
|
||||||
|
this.unlistenMouseMove = undefined;
|
||||||
this.unlistenMouseUp();
|
this.unlistenMouseUp();
|
||||||
this.resizableContainer.mouseup.next(event);
|
this.resizableContainer.mouseup.next(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,7 +406,9 @@
|
|||||||
"ICON_DISABLED": "Disabled",
|
"ICON_DISABLED": "Disabled",
|
||||||
"ROW_OPTION_BUTTON": "Actions",
|
"ROW_OPTION_BUTTON": "Actions",
|
||||||
"DRAG": "Drag button",
|
"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": {
|
"FILE_TYPE": {
|
||||||
"DOCUMENT": "Document",
|
"DOCUMENT": "Document",
|
||||||
|
|||||||
Reference in New Issue
Block a user