From 556b790248b9fa6c3b7da8650e0010674bdb8694 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Mon, 2 Dec 2024 17:00:57 -0500 Subject: [PATCH] feat(material-experimental/column-resize): Add support for "lazy" rather than live updating during resizing. For complex tables, live resizing is laggy and difficult to use. Keeping the current behavior as default, but we may want to revisit that going forward. --- renovate.json | 6 +-- .../column-resize/column-resize.ts | 27 ++++++++++- .../column-resize/overlay-handle.ts | 46 ++++++++++++++----- .../column-resize/resizable.ts | 1 + .../column-resize/resize-ref.ts | 1 + .../column-resize/column-resize.spec.ts | 40 +++++++++++++++- .../column-resize/public-api.ts | 2 + 7 files changed, 104 insertions(+), 19 deletions(-) diff --git a/renovate.json b/renovate.json index 0dd970b703c8..b492a6face4a 100644 --- a/renovate.json +++ b/renovate.json @@ -24,11 +24,7 @@ "matchPackageNames": ["*"] }, { - "matchPackageNames": [ - "@angular/ng-dev", - "@angular/build-tooling", - "angular/dev-infra" - ], + "matchPackageNames": ["@angular/ng-dev", "@angular/build-tooling", "angular/dev-infra"], "groupName": "angular shared dev-infra code", "enabled": true }, diff --git a/src/cdk-experimental/column-resize/column-resize.ts b/src/cdk-experimental/column-resize/column-resize.ts index 0ec6b7a7c3fc..bf83a770315d 100644 --- a/src/cdk-experimental/column-resize/column-resize.ts +++ b/src/cdk-experimental/column-resize/column-resize.ts @@ -6,7 +6,16 @@ * found in the LICENSE file at https://angular.dev/license */ -import {AfterViewInit, Directive, ElementRef, inject, NgZone, OnDestroy} from '@angular/core'; +import { + AfterViewInit, + Directive, + ElementRef, + inject, + InjectionToken, + Input, + NgZone, + OnDestroy, +} from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {fromEvent, merge, Subject} from 'rxjs'; import {filter, map, mapTo, pairwise, startWith, take, takeUntil} from 'rxjs/operators'; @@ -20,6 +29,15 @@ import {HeaderRowEventDispatcher} from './event-dispatcher'; const HOVER_OR_ACTIVE_CLASS = 'cdk-column-resize-hover-or-active'; const WITH_RESIZED_COLUMN_CLASS = 'cdk-column-resize-with-resized-column'; +/** Configurable options for column resize. */ +export interface ColumnResizeOptions { + liveResizeUpdates?: boolean; // Defaults to true. +} + +export const COLUMN_RESIZE_OPTIONS = new InjectionToken( + 'CdkColumnResizeOptions', +); + /** * Base class for ColumnResize directives which attach to mat-table elements to * provide common events and services for column resizing. @@ -45,6 +63,13 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy { /** The id attribute of the table, if specified. */ id?: string; + /** + * Whether to update the column's width continuously as the mouse position + * changes, or to wait until mouseup to apply the new size. + */ + @Input() liveResizeUpdates = + inject(COLUMN_RESIZE_OPTIONS, {optional: true})?.liveResizeUpdates ?? true; + ngAfterViewInit() { this.elementRef.nativeElement!.classList.add(this.getUniqueCssClass()); diff --git a/src/cdk-experimental/column-resize/overlay-handle.ts b/src/cdk-experimental/column-resize/overlay-handle.ts index e5d7cd079c72..5ea1985a5b05 100644 --- a/src/cdk-experimental/column-resize/overlay-handle.ts +++ b/src/cdk-experimental/column-resize/overlay-handle.ts @@ -49,6 +49,8 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { protected abstract readonly resizeRef: ResizeRef; protected abstract readonly styleScheduler: _CoalescedStyleScheduler; + private _cumulativeDeltaX = 0; + ngAfterViewInit() { this._listenForMouseEvents(); } @@ -101,6 +103,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { let originOffset = this._getOriginOffset(); let size = initialSize; let overshot = 0; + this._cumulativeDeltaX = 0; this.updateResizeActive(true); @@ -125,6 +128,14 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { .subscribe(([prevX, currX]) => { let deltaX = currX - prevX; + if (!this.resizeRef.liveUpdates) { + this._cumulativeDeltaX += deltaX; + const sizeDelta = this._computeNewSize(size, this._cumulativeDeltaX) - size; + this._updateOverlayOffset(sizeDelta); + + return; + } + // If the mouse moved further than the resize was able to match, limit the // movement of the overlay to match the actual size and position of the origin. if (overshot !== 0) { @@ -143,18 +154,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { } } - let computedNewSize: number = size + (this._isLtr() ? deltaX : -deltaX); - computedNewSize = Math.min( - Math.max(computedNewSize, this.resizeRef.minWidthPx, 0), - this.resizeRef.maxWidthPx, - ); - - this.resizeNotifier.triggerResize.next({ - columnId: this.columnDef.name, - size: computedNewSize, - previousSize: size, - isStickyColumn: this.columnDef.sticky || this.columnDef.stickyEnd, - }); + this._triggerResize(size, deltaX); this.styleScheduler.scheduleEnd(() => { const originNewSize = this._getOriginWidth(); @@ -178,6 +178,24 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { ); } + private _triggerResize(startSize: number, deltaX: number): void { + this.resizeNotifier.triggerResize.next({ + columnId: this.columnDef.name, + size: this._computeNewSize(startSize, deltaX), + previousSize: startSize, + isStickyColumn: this.columnDef.sticky || this.columnDef.stickyEnd, + }); + } + + private _computeNewSize(startSize: number, deltaX: number): number { + let computedNewSize: number = startSize + (this._isLtr() ? deltaX : -deltaX); + computedNewSize = Math.min( + Math.max(computedNewSize, this.resizeRef.minWidthPx, 0), + this.resizeRef.maxWidthPx, + ); + return computedNewSize; + } + private _getOriginWidth(): number { return this.resizeRef.origin.nativeElement!.offsetWidth; } @@ -202,6 +220,10 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy { this.ngZone.run(() => { const sizeMessage = {columnId: this.columnDef.name, size}; if (completedSuccessfully) { + if (!this.resizeRef.liveUpdates) { + this._triggerResize(size, this._cumulativeDeltaX); + } + this.resizeNotifier.resizeCompleted.next(sizeMessage); } else { this.resizeNotifier.resizeCanceled.next(sizeMessage); diff --git a/src/cdk-experimental/column-resize/resizable.ts b/src/cdk-experimental/column-resize/resizable.ts index a71a49e616f7..94ff2c766f77 100644 --- a/src/cdk-experimental/column-resize/resizable.ts +++ b/src/cdk-experimental/column-resize/resizable.ts @@ -230,6 +230,7 @@ export abstract class Resizable this.overlayRef!, this.minWidthPx, this.maxWidthPx, + this.columnResize.liveResizeUpdates, ), }, ], diff --git a/src/cdk-experimental/column-resize/resize-ref.ts b/src/cdk-experimental/column-resize/resize-ref.ts index efc02c6b6495..ce95c9cb2db3 100644 --- a/src/cdk-experimental/column-resize/resize-ref.ts +++ b/src/cdk-experimental/column-resize/resize-ref.ts @@ -16,5 +16,6 @@ export class ResizeRef { readonly overlayRef: OverlayRef, readonly minWidthPx: number, readonly maxWidthPx: number, + readonly liveUpdates = true, ) {} } diff --git a/src/material-experimental/column-resize/column-resize.spec.ts b/src/material-experimental/column-resize/column-resize.spec.ts index 3ea285d882d5..554d36c80874 100644 --- a/src/material-experimental/column-resize/column-resize.spec.ts +++ b/src/material-experimental/column-resize/column-resize.spec.ts @@ -436,7 +436,7 @@ describe('Material Popover Edit', () => { expect(component.getOverlayThumbElement(0)).toBeUndefined(); })); - it('resizes the target column via mouse input', fakeAsync(() => { + it('resizes the target column via mouse input (live updates)', fakeAsync(() => { const initialTableWidth = component.getTableWidth(); const initialColumnWidth = component.getColumnWidth(1); const initialColumnPosition = component.getColumnOriginPosition(1); @@ -485,6 +485,44 @@ describe('Material Popover Edit', () => { fixture.detectChanges(); })); + it('resizes the target column via mouse input (no live update)', fakeAsync(() => { + const initialTableWidth = component.getTableWidth(); + const initialColumnWidth = component.getColumnWidth(1); + + component.columnResize.liveResizeUpdates = false; + + component.triggerHoverState(); + fixture.detectChanges(); + component.beginColumnResizeWithMouse(1); + + const initialThumbPosition = component.getOverlayThumbPosition(1); + component.updateResizeWithMouseInProgress(5); + fixture.detectChanges(); + flush(); + + let thumbPositionDelta = component.getOverlayThumbPosition(1) - initialThumbPosition; + (expect(thumbPositionDelta) as any).isApproximately(5); + (expect(component.getColumnWidth(1)) as any).toBe(initialColumnWidth); + + component.updateResizeWithMouseInProgress(1); + fixture.detectChanges(); + flush(); + + thumbPositionDelta = component.getOverlayThumbPosition(1) - initialThumbPosition; + + (expect(component.getTableWidth()) as any).toBe(initialTableWidth); + (expect(component.getColumnWidth(1)) as any).toBe(initialColumnWidth); + + component.completeResizeWithMouseInProgress(1); + flush(); + + (expect(component.getTableWidth()) as any).isApproximately(initialTableWidth + 1); + (expect(component.getColumnWidth(1)) as any).isApproximately(initialColumnWidth + 1); + + component.endHoverState(); + fixture.detectChanges(); + })); + it('should not start dragging using the right mouse button', fakeAsync(() => { const initialColumnWidth = component.getColumnWidth(1); diff --git a/src/material-experimental/column-resize/public-api.ts b/src/material-experimental/column-resize/public-api.ts index 6c8c6d3caec9..292bb3e81938 100644 --- a/src/material-experimental/column-resize/public-api.ts +++ b/src/material-experimental/column-resize/public-api.ts @@ -15,3 +15,5 @@ export * from './resizable-directives/default-enabled-resizable'; export * from './resizable-directives/resizable'; export * from './resize-strategy'; export * from './overlay-handle'; +export type {ColumnResizeOptions} from '@angular/cdk-experimental/column-resize'; +export {COLUMN_RESIZE_OPTIONS} from '@angular/cdk-experimental/column-resize';