Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(cdk-experimental/column-resize): Use ResizeObserver to avoid lay… #30215

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/cdk-experimental/column-resize/resizable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Injector,
NgZone,
OnDestroy,
OnInit,
Type,
ViewContainerRef,
ChangeDetectorRef,
Expand Down Expand Up @@ -44,7 +45,7 @@ const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
*/
@Directive()
export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
implements AfterViewInit, OnDestroy
implements AfterViewInit, OnDestroy, OnInit
{
protected minWidthPxInternal: number = 0;
protected maxWidthPxInternal: number = Number.MAX_SAFE_INTEGER;
Expand Down Expand Up @@ -99,6 +100,10 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
}
}

ngOnInit() {
this.resizeStrategy.registerColumn(this.elementRef.nativeElement);
}

ngAfterViewInit() {
this._listenForRowHoverEvents();
this._listenForResizeEvents();
Expand Down Expand Up @@ -310,14 +315,13 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
}

private _appendInlineHandle(): void {
this.styleScheduler.schedule(() => {
this.inlineHandle = this.document.createElement('div');
this.inlineHandle.tabIndex = 0;
this.inlineHandle.className = this.getInlineHandleCssClassName();
this.inlineHandle = this.document.createElement('div');
// TODO: re-apply tab index once this element has behavior.
// this.inlineHandle.tabIndex = 0;
this.inlineHandle.className = this.getInlineHandleCssClassName();

// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.

this.elementRef.nativeElement!.appendChild(this.inlineHandle);
});
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
}
}
67 changes: 54 additions & 13 deletions src/cdk-experimental/column-resize/resize-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ import {ColumnResize} from './column-resize';
* The details of how resizing works for tables for flex mat-tables are quite different.
*/
@Injectable()
export abstract class ResizeStrategy {
export abstract class ResizeStrategy implements OnDestroy {
protected abstract readonly columnResize: ColumnResize;
protected abstract readonly styleScheduler: _CoalescedStyleScheduler;
protected abstract readonly table: CdkTable<unknown>;

private _pendingResizeDelta: number | null = null;
private _tableObserved = false;
private _elemSizeCache = new WeakMap<HTMLElement, {width: number; height: number}>();
private _resizeObserver = globalThis?.ResizeObserver
? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries))
: null;

/** Updates the width of the specified column. */
abstract applyColumnSize(
Expand Down Expand Up @@ -51,7 +56,7 @@ export abstract class ResizeStrategy {
protected updateTableWidthAndStickyColumns(delta: number): void {
if (this._pendingResizeDelta === null) {
const tableElement = this.columnResize.elementRef.nativeElement;
const tableWidth = getElementWidth(tableElement);
const tableWidth = this.getElementWidth(tableElement);

this.styleScheduler.schedule(() => {
tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta!);
Expand All @@ -66,6 +71,48 @@ export abstract class ResizeStrategy {

this._pendingResizeDelta = (this._pendingResizeDelta ?? 0) + delta;
}

/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
protected getElementWidth(element: HTMLElement) {
// Optimization: Check style.width first as we probably set it already before reading
// offsetWidth which triggers layout.
return (
coercePixelsFromCssValue(element.style.width) ||
this._elemSizeCache.get(element)?.width ||
element.offsetWidth
);
}

/** Informs the ResizeStrategy instance of a column that may be resized in the future. */
registerColumn(column: HTMLElement) {
if (!this._tableObserved) {
this._tableObserved = true;
this._resizeObserver?.observe(this.columnResize.elementRef.nativeElement, {
box: 'border-box',
});
}
this._resizeObserver?.observe(column, {box: 'border-box'});
}

ngOnDestroy(): void {
this._resizeObserver?.disconnect();
}

private _updateCachedSizes(entries: ResizeObserverEntry[]) {
for (const entry of entries) {
const newEntry = entry.borderBoxSize?.length
? {
width: entry.borderBoxSize[0].inlineSize,
height: entry.borderBoxSize[0].blockSize,
}
: {
width: entry.contentRect.width,
height: entry.contentRect.height,
};

this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
}
}
}

/**
Expand All @@ -87,7 +134,7 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
sizeInPx: number,
previousSizeInPx?: number,
): void {
const delta = sizeInPx - (previousSizeInPx ?? getElementWidth(columnHeader));
const delta = sizeInPx - (previousSizeInPx ?? this.getElementWidth(columnHeader));

if (delta === 0) {
return;
Expand All @@ -101,14 +148,14 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
}

applyMinColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
const currentWidth = getElementWidth(columnHeader);
const currentWidth = this.getElementWidth(columnHeader);
const newWidth = Math.max(currentWidth, sizeInPx);

this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
}

applyMaxColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
const currentWidth = getElementWidth(columnHeader);
const currentWidth = this.getElementWidth(columnHeader);
const newWidth = Math.min(currentWidth, sizeInPx);

this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
Expand Down Expand Up @@ -189,7 +236,8 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
return `cdk-column-${cssFriendlyColumnName}`;
}

ngOnDestroy(): void {
override ngOnDestroy(): void {
super.ngOnDestroy();
this._styleElement?.remove();
this._styleElement = undefined;
}
Expand Down Expand Up @@ -277,13 +325,6 @@ function coercePixelsFromCssValue(cssValue: string): number {
return Number(cssValue.match(/(\d+)px/)?.[1]);
}

/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
function getElementWidth(element: HTMLElement) {
// Optimization: Check style.width first as we probably set it already before reading
// offsetWidth which triggers layout.
return coercePixelsFromCssValue(element.style.width) || element.offsetWidth;
}

/**
* Converts CSS flex values as set in CdkFlexTableResizeStrategy to numbers,
* eg "0 0.01 123px" to 123.
Expand Down
Loading