From 8685c01a92e057b20d502e5163561391dcc7fb21 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 4 Dec 2024 15:12:48 -0500 Subject: [PATCH] feat(material-experimental/column-resize): Add support for "lazy" rather than live updating during resizing. (#30120) 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';