Skip to content

Commit 781051b

Browse files
authored
fix(column-resize): Resize table as well as columns, improve handing … (#19264)
1 parent fe87bda commit 781051b

23 files changed

+191
-77
lines changed

src/cdk-experimental/column-resize/column-resize-directives/column-resize-flex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {FLEX_PROVIDERS} from './constants';
2727
export class CdkColumnResizeFlex extends ColumnResize {
2828
constructor(
2929
readonly columnResizeNotifier: ColumnResizeNotifier,
30-
protected readonly elementRef: ElementRef,
30+
readonly elementRef: ElementRef<HTMLElement>,
3131
protected readonly eventDispatcher: HeaderRowEventDispatcher,
3232
protected readonly ngZone: NgZone,
3333
protected readonly notifier: ColumnResizeNotifierSource) {

src/cdk-experimental/column-resize/column-resize-directives/column-resize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {TABLE_PROVIDERS} from './constants';
2727
export class CdkColumnResize extends ColumnResize {
2828
constructor(
2929
readonly columnResizeNotifier: ColumnResizeNotifier,
30-
protected readonly elementRef: ElementRef,
30+
readonly elementRef: ElementRef<HTMLElement>,
3131
protected readonly eventDispatcher: HeaderRowEventDispatcher,
3232
protected readonly ngZone: NgZone,
3333
protected readonly notifier: ColumnResizeNotifierSource) {

src/cdk-experimental/column-resize/column-resize-directives/default-enabled-column-resize-flex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {FLEX_PROVIDERS} from './constants';
2727
export class CdkDefaultEnabledColumnResizeFlex extends ColumnResize {
2828
constructor(
2929
readonly columnResizeNotifier: ColumnResizeNotifier,
30-
protected readonly elementRef: ElementRef,
30+
readonly elementRef: ElementRef<HTMLElement>,
3131
protected readonly eventDispatcher: HeaderRowEventDispatcher,
3232
protected readonly ngZone: NgZone,
3333
protected readonly notifier: ColumnResizeNotifierSource) {

src/cdk-experimental/column-resize/column-resize-directives/default-enabled-column-resize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {TABLE_PROVIDERS} from './constants';
2727
export class CdkDefaultEnabledColumnResize extends ColumnResize {
2828
constructor(
2929
readonly columnResizeNotifier: ColumnResizeNotifier,
30-
protected readonly elementRef: ElementRef,
30+
readonly elementRef: ElementRef<HTMLElement>,
3131
protected readonly eventDispatcher: HeaderRowEventDispatcher,
3232
protected readonly ngZone: NgZone,
3333
protected readonly notifier: ColumnResizeNotifierSource) {

src/cdk-experimental/column-resize/column-resize-notifier.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface ColumnSize {
1616

1717
/** The width in pixels of the column. */
1818
readonly size: number;
19+
20+
/** The width in pixels of the column prior to this update, if known. */
21+
readonly previousSize?: number;
1922
}
2023

2124
/** Interface describing column size changes. */
@@ -28,7 +31,10 @@ export interface ColumnSizeAction extends ColumnSize {
2831
readonly completeImmediately?: boolean;
2932
}
3033

31-
/** Originating source of column resize events within a table. */
34+
/**
35+
* Originating source of column resize events within a table.
36+
* @docs-private
37+
*/
3238
@Injectable()
3339
export class ColumnResizeNotifierSource {
3440
/** Emits when an in-progress resize is canceled. */

src/cdk-experimental/column-resize/column-resize.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy {
3232
/* Publicly accessible interface for triggering and being notified of resizes. */
3333
abstract readonly columnResizeNotifier: ColumnResizeNotifier;
3434

35-
protected abstract readonly elementRef: ElementRef<HTMLElement>;
35+
/* ElementRef that this directive is attached to. Exposed For use by column-level directives */
36+
abstract readonly elementRef: ElementRef<HTMLElement>;
37+
3638
protected abstract readonly eventDispatcher: HeaderRowEventDispatcher;
3739
protected abstract readonly ngZone: NgZone;
3840
protected abstract readonly notifier: ColumnResizeNotifierSource;
@@ -61,6 +63,11 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy {
6163
return `cdk-column-resize-${this.selectorId}`;
6264
}
6365

66+
/** Called when a column in the table is resized. Applies a css class to the table element. */
67+
setResized() {
68+
this.elementRef.nativeElement!.classList.add(WITH_RESIZED_COLUMN_CLASS);
69+
}
70+
6471
private _listenForRowHoverEvents() {
6572
this.ngZone.runOutsideAngular(() => {
6673
const element = this.elementRef.nativeElement!;
@@ -87,7 +94,7 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy {
8794
takeUntil(this.destroyed),
8895
take(1),
8996
).subscribe(() => {
90-
this.elementRef.nativeElement!.classList.add(WITH_RESIZED_COLUMN_CLASS);
97+
this.setResized();
9198
});
9299
}
93100

src/cdk-experimental/column-resize/overlay-handle.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,8 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
9292

9393
const startX = mousedownEvent.screenX;
9494

95-
const initialOverlayOffset = this._getOverlayOffset();
9695
const initialSize = this._getOriginWidth();
97-
let overlayOffset = initialOverlayOffset;
96+
let overlayOffset = this._getOverlayOffset();
9897
let originOffset = this._getOriginOffset();
9998
let size = initialSize;
10099
let overshot = 0;
@@ -143,7 +142,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
143142
Math.max(computedNewSize, this.resizeRef.minWidthPx, 0), this.resizeRef.maxWidthPx);
144143

145144
this.resizeNotifier.triggerResize.next(
146-
{columnId: this.columnDef.name, size: computedNewSize});
145+
{columnId: this.columnDef.name, size: computedNewSize, previousSize: size});
147146

148147
const originNewSize = this._getOriginWidth();
149148
const originNewOffset = this._getOriginOffset();
@@ -153,7 +152,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
153152
originOffset = originNewOffset;
154153

155154
overshot += deltaX + (this._isLtr() ? -originSizeDeltaX : originSizeDeltaX);
156-
overlayOffset += originSizeDeltaX + originOffsetDeltaX;
155+
overlayOffset += originOffsetDeltaX + (this._isLtr() ? originSizeDeltaX : 0);
157156

158157
this._updateOverlayOffset(overlayOffset);
159158
});
@@ -169,29 +168,15 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
169168
}
170169

171170
private _getOriginOffset(): number {
172-
const originElement = this.resizeRef.origin.nativeElement!;
173-
const offsetLeft = originElement.offsetLeft;
174-
175-
return this._isLtr() ?
176-
offsetLeft :
177-
originElement.offsetParent!.offsetWidth - (offsetLeft + this._getOriginWidth());
171+
return this.resizeRef.origin.nativeElement!.offsetLeft;
178172
}
179173

180174
private _getOverlayOffset(): number {
181-
const overlayElement = this.resizeRef.overlayRef.overlayElement;
182-
return this._isLtr() ?
183-
parseInt(overlayElement.style.left!, 10) : parseInt(overlayElement.style.right!, 10);
175+
return parseInt(this.resizeRef.overlayRef.overlayElement.style.left!, 10);
184176
}
185177

186178
private _updateOverlayOffset(offset: number): void {
187-
const overlayElement = this.resizeRef.overlayRef.overlayElement;
188-
const overlayOffsetCssValue = coerceCssPixelValue(offset);
189-
190-
if (this._isLtr()) {
191-
overlayElement.style.left = overlayOffsetCssValue;
192-
} else {
193-
overlayElement.style.right = overlayOffsetCssValue;
194-
}
179+
this.resizeRef.overlayRef.overlayElement.style.left = coerceCssPixelValue(offset);
195180
}
196181

197182
private _isLtr(): boolean {

src/cdk-experimental/column-resize/resizable.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
7070
this.minWidthPxInternal = value;
7171

7272
if (this.elementRef.nativeElement) {
73+
this.columnResize.setResized();
7374
this._applyMinWidthPx();
7475
}
7576
}
@@ -82,6 +83,7 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
8283
this.maxWidthPxInternal = value;
8384

8485
if (this.elementRef.nativeElement) {
86+
this.columnResize.setResized();
8587
this._applyMaxWidthPx();
8688
}
8789
}
@@ -116,20 +118,23 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
116118
// of two table cells and is also useful for displaying a resize thumb
117119
// over both cells and extending it down the table as needed.
118120

121+
const isRtl = this.directionality.value === 'rtl';
119122
const positionStrategy = this.overlay.position()
120123
.flexibleConnectedTo(this.elementRef.nativeElement!)
121124
.withFlexibleDimensions(false)
122125
.withGrowAfterOpen(false)
123126
.withPush(false)
127+
.withDefaultOffsetX(isRtl ? 1 : 0)
124128
.withPositions([{
125-
originX: 'end',
129+
originX: isRtl ? 'start' : 'end',
126130
originY: 'top',
127131
overlayX: 'center',
128132
overlayY: 'top',
129133
}]);
130134

131135
return this.overlay.create({
132-
direction: this.directionality,
136+
// Always position the overlay based on left-indexed coordinates.
137+
direction: 'ltr',
133138
disposeOnNavigation: true,
134139
positionStrategy,
135140
scrollStrategy: this.overlay.scrollStrategies.reposition(),
@@ -166,9 +171,9 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
166171
).pipe(
167172
takeUntilDestroyed,
168173
filter(columnSize => columnSize.columnId === this.columnDef.name),
169-
).subscribe(({size, completeImmediately}) => {
174+
).subscribe(({size, previousSize, completeImmediately}) => {
170175
this.elementRef.nativeElement!.classList.add(OVERLAY_ACTIVE_CLASS);
171-
this._applySize(size);
176+
this._applySize(size, previousSize);
172177

173178
if (completeImmediately) {
174179
this._completeResizeOperation();
@@ -223,11 +228,11 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
223228
this.overlayRef!.updateSize({height: this.elementRef.nativeElement!.offsetHeight});
224229
}
225230

226-
private _applySize(sizeInPixels: number): void {
231+
private _applySize(sizeInPixels: number, previousSize?: number): void {
227232
const sizeToApply = Math.min(Math.max(sizeInPixels, this.minWidthPx, 0), this.maxWidthPx);
228233

229234
this.resizeStrategy.applyColumnSize(this.columnDef.cssClassFriendlyName,
230-
this.elementRef.nativeElement!, sizeToApply);
235+
this.elementRef.nativeElement!, sizeToApply, previousSize);
231236
}
232237

233238
private _applyMinWidthPx(): void {

src/cdk-experimental/column-resize/resize-strategy.ts

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,34 @@ import {ColumnResize} from './column-resize';
1818
*/
1919
@Injectable()
2020
export abstract class ResizeStrategy {
21+
protected abstract readonly columnResize: ColumnResize;
22+
23+
/** Updates the width of the specified column. */
2124
abstract applyColumnSize(
2225
cssFriendlyColumnName: string,
2326
columnHeader: HTMLElement,
24-
sizeInPx: number): void;
27+
sizeInPx: number,
28+
previousSizeInPx?: number): void;
2529

30+
/** Applies a minimum width to the specified column, updating its current width as needed. */
2631
abstract applyMinColumnSize(
2732
cssFriendlyColumnName: string,
2833
columnHeader: HTMLElement,
2934
minSizeInPx: number): void;
3035

36+
/** Applies a maximum width to the specified column, updating its current width as needed. */
3137
abstract applyMaxColumnSize(
3238
cssFriendlyColumnName: string,
3339
columnHeader: HTMLElement,
3440
minSizeInPx: number): void;
41+
42+
/** Adjusts the width of the table element by the specified delta. */
43+
protected updateTableWidth(delta: number): void {
44+
const table = this.columnResize.elementRef.nativeElement;
45+
const tableWidth = getElementWidth(table);
46+
47+
table.style.width = coerceCssPixelValue(tableWidth + delta);
48+
}
3549
}
3650

3751
/**
@@ -43,17 +57,31 @@ export abstract class ResizeStrategy {
4357
*/
4458
@Injectable()
4559
export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
46-
applyColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
60+
constructor(protected readonly columnResize: ColumnResize) {
61+
super();
62+
}
63+
64+
applyColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number,
65+
previousSizeInPx?: number): void {
66+
const delta = sizeInPx - (previousSizeInPx ?? getElementWidth(columnHeader));
67+
4768
columnHeader.style.width = coerceCssPixelValue(sizeInPx);
69+
70+
this.updateTableWidth(delta);
4871
}
4972

5073
applyMinColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
51-
columnHeader.style.minWidth = coerceCssPixelValue(sizeInPx);
74+
const currentWidth = getElementWidth(columnHeader);
75+
const newWidth = Math.max(currentWidth, sizeInPx);
76+
77+
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
5278
}
5379

54-
applyMaxColumnSize(): void {
55-
// Intentionally omitted as max-width causes strange rendering issues in Chrome.
56-
// Max size will still apply when the user is resizing this column.
80+
applyMaxColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
81+
const currentWidth = getElementWidth(columnHeader);
82+
const newWidth = Math.min(currentWidth, sizeInPx);
83+
84+
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
5785
}
5886
}
5987

@@ -76,16 +104,23 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
76104
protected readonly defaultMaxSize = Number.MAX_SAFE_INTEGER;
77105

78106
constructor(
79-
private readonly _columnResize: ColumnResize,
107+
protected readonly columnResize: ColumnResize,
80108
@Inject(DOCUMENT) document: any) {
81109
super();
82110
this._document = document;
83111
}
84112

85-
applyColumnSize(cssFriendlyColumnName: string, _: HTMLElement, sizeInPx: number): void {
113+
applyColumnSize(cssFriendlyColumnName: string, columnHeader: HTMLElement,
114+
sizeInPx: number, previousSizeInPx?: number): void {
115+
// Optimization: Check applied width first as we probably set it already before reading
116+
// offsetWidth which triggers layout.
117+
const delta = sizeInPx - (previousSizeInPx ??
118+
(this._getAppliedWidth(cssFriendlyColumnName) || columnHeader.offsetWidth));
119+
86120
const cssSize = coerceCssPixelValue(sizeInPx);
87121

88122
this._applyProperty(cssFriendlyColumnName, 'flex', `0 0.01 ${cssSize}`);
123+
this.updateTableWidth(delta);
89124
}
90125

91126
applyMinColumnSize(cssFriendlyColumnName: string, _: HTMLElement, sizeInPx: number): void {
@@ -106,14 +141,23 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
106141
return `cdk-column-${cssFriendlyColumnName}`;
107142
}
108143

109-
ngOnDestroy() {
144+
ngOnDestroy(): void {
110145
// TODO: Use remove() once we're off IE11.
111146
if (this._styleElement && this._styleElement.parentNode) {
112147
this._styleElement.parentNode.removeChild(this._styleElement);
113148
this._styleElement = undefined;
114149
}
115150
}
116151

152+
private _getPropertyValue(cssFriendlyColumnName: string, key: string): string|undefined {
153+
const properties = this._getColumnPropertiesMap(cssFriendlyColumnName);
154+
return properties.get(key);
155+
}
156+
157+
private _getAppliedWidth(cssFriendslyColumnName: string): number {
158+
return coercePixelsFromFlexValue(this._getPropertyValue(cssFriendslyColumnName, 'flex'));
159+
}
160+
117161
private _applyProperty(
118162
cssFriendlyColumnName: string,
119163
key: string,
@@ -166,7 +210,7 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
166210
}
167211

168212
const columnClassName = this.getColumnCssClass(cssFriendlyColumnName);
169-
const tableClassName = this._columnResize.getUniqueCssClass();
213+
const tableClassName = this.columnResize.getUniqueCssClass();
170214

171215
const selector = `.${tableClassName} .${columnClassName}`;
172216
const body = propertyKeys.map(key => `${key}:${properties.get(key)}`).join(';');
@@ -175,6 +219,26 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
175219
}
176220
}
177221

222+
/** Converts CSS pixel values to numbers, eg "123px" to 123. Returns NaN for non pixel values. */
223+
function coercePixelsFromCssValue(cssValue: string): number {
224+
return Number(cssValue.match(/(\d+)px/)?.[1]);
225+
}
226+
227+
/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
228+
function getElementWidth(element: HTMLElement) {
229+
// Optimization: Check style.width first as we probably set it already before reading
230+
// offsetWidth which triggers layout.
231+
return coercePixelsFromCssValue(element.style.width) || element.offsetWidth;
232+
}
233+
234+
/**
235+
* Converts CSS flex values as set in CdkFlexTableResizeStrategy to numbers,
236+
* eg "0 0.01 123px" to 123.
237+
*/
238+
function coercePixelsFromFlexValue(flexValue: string|undefined): number {
239+
return Number(flexValue?.match(/0 0\.01 (\d+)px/)?.[1]);
240+
}
241+
178242
export const TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER: Provider = {
179243
provide: ResizeStrategy,
180244
useClass: TableLayoutFixedResizeStrategy,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:host {
2+
display: block;
3+
overflow: auto;
4+
}

src/components-examples/material-experimental/column-resize/default-enabled-flex/default-enabled-column-resize-flex-demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<!-- Name Column -->
99
<ng-container matColumnDef="name">
10-
<mat-header-cell *matHeaderCellDef [matResizableMinWidthPx]="100"> Name </mat-header-cell>
10+
<mat-header-cell *matHeaderCellDef [matResizableMinWidthPx]="150"> Name </mat-header-cell>
1111
<mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
1212
</ng-container>
1313

0 commit comments

Comments
 (0)