Skip to content

Commit 1a84770

Browse files
kseamonandrewseguin
authored andcommitted
feat(popover-edit): Adds support for spanning multiple columns and setting width of the popup based on the size of the cell(s) (#15724)
1 parent 73c4bed commit 1a84770

8 files changed

+340
-28
lines changed

src/cdk-experimental/popover-edit/popover-edit-position-strategy-factory.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,27 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ElementRef, Injectable} from '@angular/core';
10-
import {Overlay, PositionStrategy} from '@angular/cdk/overlay';
9+
import {Directionality} from '@angular/cdk/bidi';
10+
import {Overlay, OverlaySizeConfig, PositionStrategy} from '@angular/cdk/overlay';
11+
import {Injectable} from '@angular/core';
1112

1213
/**
13-
* Overridable factory responsible for configuring how cdkPopoverEdit popovers are positioned.
14+
* Overridable factory responsible for configuring how cdkPopoverEdit popovers are positioned
15+
* and sized.
1416
*/
1517
@Injectable()
1618
export abstract class PopoverEditPositionStrategyFactory {
17-
abstract forElementRef(elementRef: ElementRef): PositionStrategy;
19+
/**
20+
* Creates a PositionStrategy based on the specified table cells.
21+
* The cells will be provided in DOM order.
22+
*/
23+
abstract positionStrategyForCells(cells: HTMLElement[]): PositionStrategy;
24+
25+
/**
26+
* Creates an OverlaySizeConfig based on the specified table cells.
27+
* The cells will be provided in DOM order.
28+
*/
29+
abstract sizeConfigForCells(cells: HTMLElement[]): OverlaySizeConfig;
1830
}
1931

2032
/**
@@ -24,19 +36,42 @@ export abstract class PopoverEditPositionStrategyFactory {
2436
*/
2537
@Injectable()
2638
export class DefaultPopoverEditPositionStrategyFactory extends PopoverEditPositionStrategyFactory {
27-
constructor(protected readonly overlay: Overlay) {
39+
constructor(protected readonly direction: Directionality, protected readonly overlay: Overlay) {
2840
super();
2941
}
3042

31-
forElementRef(elementRef: ElementRef): PositionStrategy {
32-
return this.overlay.position().flexibleConnectedTo(elementRef)
43+
positionStrategyForCells(cells: HTMLElement[]): PositionStrategy {
44+
return this.overlay.position()
45+
.flexibleConnectedTo(cells[0])
3346
.withGrowAfterOpen()
3447
.withPush()
48+
.withViewportMargin(16)
3549
.withPositions([{
3650
originX: 'start',
3751
originY: 'top',
3852
overlayX: 'start',
3953
overlayY: 'top',
4054
}]);
4155
}
56+
57+
sizeConfigForCells(cells: HTMLElement[]): OverlaySizeConfig {
58+
if (cells.length === 0) {
59+
return {};
60+
}
61+
62+
if (cells.length === 1) {
63+
return {width: cells[0].getBoundingClientRect().width};
64+
}
65+
66+
let firstCell, lastCell;
67+
if (this.direction.value === 'ltr') {
68+
firstCell = cells[0];
69+
lastCell = cells[cells.length - 1];
70+
} else {
71+
lastCell = cells[0];
72+
firstCell = cells[cells.length - 1];
73+
}
74+
75+
return {width: lastCell.getBoundingClientRect().right - firstCell.getBoundingClientRect().left};
76+
}
4277
}

src/cdk-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {BehaviorSubject} from 'rxjs';
2-
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
3-
import {CommonModule} from '@angular/common';
4-
import {FormsModule, NgForm} from '@angular/forms';
5-
import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
61
import {DataSource} from '@angular/cdk/collections';
72
import {CdkTableModule} from '@angular/cdk/table';
8-
import {CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index';
3+
import {CommonModule} from '@angular/common';
4+
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
5+
import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
6+
import {FormsModule, NgForm} from '@angular/forms';
7+
import {BehaviorSubject} from 'rxjs';
8+
9+
import {CdkPopoverEditColspan, CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index';
910

1011
const EDIT_TEMPLATE = `
1112
<div style="background-color: white;">
@@ -33,6 +34,8 @@ const CELL_TEMPLATE = `
3334
</span>
3435
`;
3536

37+
const POPOVER_EDIT_DIRECTIVE = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`;
38+
3639
interface PeriodicElement {
3740
name: string;
3841
weight: number;
@@ -45,6 +48,7 @@ abstract class BaseTestComponent {
4548

4649
ignoreSubmitUnlessValid = true;
4750
clickOutBehavior: PopoverEditClickOutBehavior = 'close';
51+
colspan: CdkPopoverEditColspan = {};
4852

4953
onSubmit(element: PeriodicElement, form: NgForm) {
5054
if (!form.valid) { return; }
@@ -62,7 +66,7 @@ abstract class BaseTestComponent {
6266

6367
getEditCell(rowIndex = 0) {
6468
const row = getRows(this.table.nativeElement)[rowIndex];
65-
return getCells(row)[0];
69+
return getCells(row)[1];
6670
}
6771

6872
focusEditCell(rowIndex = 0) {
@@ -84,6 +88,10 @@ abstract class BaseTestComponent {
8488
flush();
8589
}
8690

91+
getEditPane() {
92+
return document.querySelector('.cdk-edit-pane');
93+
}
94+
8795
getInput() {
8896
return document.querySelector('input') as HTMLInputElement|null;
8997
}
@@ -125,7 +133,10 @@ abstract class BaseTestComponent {
125133
</ng-template>
126134
127135
<tr *ngFor="let element of elements">
128-
<td [cdkPopoverEdit]="nameEdit" [cdkPopoverEditContext]="element">
136+
<td> just a cell </td>
137+
138+
<td ${POPOVER_EDIT_DIRECTIVE}
139+
[cdkPopoverEditContext]="element">
129140
${CELL_TEMPLATE}
130141
</td>
131142
@@ -142,7 +153,9 @@ class VanillaTableOutOfCell extends BaseTestComponent {
142153
template: `
143154
<table #table editable>
144155
<tr *ngFor="let element of elements">
145-
<td [cdkPopoverEdit]="nameEdit">
156+
<td> just a cell </td>
157+
158+
<td ${POPOVER_EDIT_DIRECTIVE}>
146159
${CELL_TEMPLATE}
147160
148161
<ng-template #nameEdit>
@@ -175,9 +188,15 @@ class ElementDataSource extends DataSource<PeriodicElement> {
175188
template: `
176189
<div #table>
177190
<cdk-table cdk-table editable [dataSource]="dataSource">
191+
<ng-container cdkColumnDef="before">
192+
<cdk-cell *cdkCellDef="let element">
193+
just a cell
194+
</cdk-cell>
195+
</ng-container>
196+
178197
<ng-container cdkColumnDef="name">
179198
<cdk-cell *cdkCellDef="let element"
180-
[cdkPopoverEdit]="nameEdit">
199+
${POPOVER_EDIT_DIRECTIVE}>
181200
${CELL_TEMPLATE}
182201
183202
<ng-template #nameEdit>
@@ -202,17 +221,23 @@ class ElementDataSource extends DataSource<PeriodicElement> {
202221
`
203222
})
204223
class CdkFlexTableInCell extends BaseTestComponent {
205-
displayedColumns = ['name', 'weight'];
224+
displayedColumns = ['before', 'name', 'weight'];
206225
dataSource = new ElementDataSource();
207226
}
208227

209228
@Component({
210229
template: `
211230
<div #table>
212231
<table cdk-table editable [dataSource]="dataSource">
232+
<ng-container cdkColumnDef="before">
233+
<td cdk-cell *cdkCellDef="let element">
234+
just a cell
235+
</td>
236+
</ng-container>
237+
213238
<ng-container cdkColumnDef="name">
214239
<td cdk-cell *cdkCellDef="let element"
215-
[cdkPopoverEdit]="nameEdit">
240+
${POPOVER_EDIT_DIRECTIVE}>
216241
${CELL_TEMPLATE}
217242
218243
<ng-template #nameEdit>
@@ -237,7 +262,7 @@ class CdkFlexTableInCell extends BaseTestComponent {
237262
`
238263
})
239264
class CdkTableInCell extends BaseTestComponent {
240-
displayedColumns = ['name', 'weight'];
265+
displayedColumns = ['before', 'name', 'weight'];
241266
dataSource = new ElementDataSource();
242267
}
243268

@@ -316,6 +341,49 @@ describe('CDK Popover Edit', () => {
316341
expect(component.getInput()!.value).toBe('Hydrogen');
317342
}));
318343

344+
it('positions the lens at the top left corner and spans the full width of the cell',
345+
fakeAsync(() => {
346+
component.openLens();
347+
348+
const paneRect = component.getEditPane()!.getBoundingClientRect();
349+
const cellRect = component.getEditCell().getBoundingClientRect();
350+
351+
expect(paneRect.width).toBe(cellRect.width);
352+
expect(paneRect.left).toBe(cellRect.left);
353+
expect(paneRect.top).toBe(cellRect.top);
354+
}));
355+
356+
it('adjusts the positioning of the lens based on colspan', fakeAsync(() => {
357+
const cellRects = getCells(getRows(component.table.nativeElement)[0])
358+
.map(cell => cell.getBoundingClientRect());
359+
360+
component.colspan = {before: 1};
361+
fixture.detectChanges();
362+
363+
component.openLens();
364+
365+
let paneRect = component.getEditPane()!.getBoundingClientRect();
366+
expect(paneRect.top).toBe(cellRects[0].top);
367+
expect(paneRect.left).toBe(cellRects[0].left);
368+
expect(paneRect.right).toBe(cellRects[1].right);
369+
370+
component.colspan = {after: 1};
371+
fixture.detectChanges();
372+
373+
paneRect = component.getEditPane()!.getBoundingClientRect();
374+
expect(paneRect.top).toBe(cellRects[1].top);
375+
expect(paneRect.left).toBe(cellRects[1].left);
376+
expect(paneRect.right).toBe(cellRects[2].right);
377+
378+
component.colspan = {before: 1, after: 1};
379+
fixture.detectChanges();
380+
381+
paneRect = component.getEditPane()!.getBoundingClientRect();
382+
expect(paneRect.top).toBe(cellRects[0].top);
383+
expect(paneRect.left).toBe(cellRects[0].left);
384+
expect(paneRect.right).toBe(cellRects[2].right);
385+
}));
386+
319387
it('updates the form and submits, closing the lens', fakeAsync(() => {
320388
component.openLens();
321389

src/cdk-experimental/popover-edit/table-directives.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
9-
import {Overlay, OverlayRef} from '@angular/cdk/overlay';
9+
import {Overlay, OverlayRef, PositionStrategy} from '@angular/cdk/overlay';
1010
import {TemplatePortal} from '@angular/cdk/portal';
11+
import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling';
1112
import {
1213
AfterViewInit,
1314
Directive,
@@ -19,14 +20,24 @@ import {
1920
TemplateRef,
2021
ViewContainerRef
2122
} from '@angular/core';
22-
import {fromEvent, ReplaySubject} from 'rxjs';
23-
import {debounceTime, filter, map, mapTo, takeUntil} from 'rxjs/operators';
23+
import {fromEvent, merge, ReplaySubject} from 'rxjs';
24+
import {debounceTime, filter, map, mapTo, startWith, takeUntil} from 'rxjs/operators';
2425

2526
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
2627
import {EditEventDispatcher} from './edit-event-dispatcher';
2728
import {closest} from './polyfill';
2829
import {PopoverEditPositionStrategyFactory} from './popover-edit-position-strategy-factory';
2930

31+
/**
32+
* Describes the number of columns before and after the originating cell that the
33+
* edit popup should span. In left to right locales, before means left and after means
34+
* right. In right to left locales before means right and after means left.
35+
*/
36+
export interface CdkPopoverEditColspan {
37+
before?: number;
38+
after?: number;
39+
}
40+
3041
/**
3142
* The delay between the mouse entering a row and the mouse stopping its movement before
3243
* showing on-hover content.
@@ -111,6 +122,28 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
111122
*/
112123
@Input('cdkPopoverEditContext') context?: C;
113124

125+
/**
126+
* Specifies that the popup should cover additional table cells before and/or after
127+
* this one.
128+
*/
129+
@Input('cdkPopoverEditColspan')
130+
get colspan(): CdkPopoverEditColspan {
131+
return this._colspan;
132+
}
133+
set colspan(value: CdkPopoverEditColspan) {
134+
this._colspan = value;
135+
136+
// Recompute positioning when the colspan changes.
137+
if (this.overlayRef) {
138+
this.overlayRef.updatePositionStrategy(this._getPositionStrategy());
139+
140+
if (this.overlayRef.hasAttached()) {
141+
this._updateOverlaySize();
142+
}
143+
}
144+
}
145+
private _colspan: CdkPopoverEditColspan = {};
146+
114147
protected focusTrap?: FocusTrap;
115148
protected overlayRef?: OverlayRef;
116149
protected readonly destroyed = new ReplaySubject<void>();
@@ -122,7 +155,9 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
122155
protected readonly ngZone: NgZone,
123156
protected readonly overlay: Overlay,
124157
protected readonly positionFactory: PopoverEditPositionStrategyFactory,
125-
protected readonly viewContainerRef: ViewContainerRef) {}
158+
protected readonly scrollDispatcher: ScrollDispatcher,
159+
protected readonly viewContainerRef: ViewContainerRef,
160+
protected readonly viewportRuler: ViewportRuler) {}
126161

127162
ngAfterViewInit(): void {
128163
this._startListeningToEditEvents();
@@ -161,8 +196,8 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
161196
this.overlayRef = this.overlay.create({
162197
disposeOnNavigation: true,
163198
panelClass: EDIT_PANE_CLASS,
164-
positionStrategy: this.positionFactory.forElementRef(this.elementRef),
165-
scrollStrategy: this.overlay.scrollStrategies.reposition({autoClose: true}),
199+
positionStrategy: this._getPositionStrategy(),
200+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
166201
});
167202

168203
this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);
@@ -179,6 +214,41 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
179214
this.viewContainerRef,
180215
{$implicit: this.context}));
181216
this.focusTrap!.focusInitialElementWhenReady();
217+
218+
// Update the size of the popup initially and on subsequent changes to
219+
// scroll position and viewport size.
220+
merge(this.scrollDispatcher.scrolled(), this.viewportRuler.change())
221+
.pipe(
222+
startWith(null),
223+
takeUntil(this.overlayRef!.detachments()),
224+
takeUntil(this.destroyed),
225+
)
226+
.subscribe(() => {
227+
this._updateOverlaySize();
228+
});
229+
}
230+
231+
private _getOverlayCells(): HTMLElement[] {
232+
const cell = closest(this.elementRef.nativeElement!, CELL_SELECTOR) as HTMLElement;
233+
234+
if (!this._colspan.before && !this._colspan.after) {
235+
return [cell];
236+
}
237+
238+
const row = closest(this.elementRef.nativeElement!, ROW_SELECTOR)!;
239+
const rowCells = Array.from(row.querySelectorAll(CELL_SELECTOR)) as HTMLElement[];
240+
const ownIndex = rowCells.indexOf(cell);
241+
242+
return rowCells.slice(
243+
ownIndex - (this._colspan.before || 0), ownIndex + (this._colspan.after || 0) + 1);
244+
}
245+
246+
private _getPositionStrategy(): PositionStrategy {
247+
return this.positionFactory.positionStrategyForCells(this._getOverlayCells());
248+
}
249+
250+
private _updateOverlaySize(): void {
251+
this.overlayRef!.updateSize(this.positionFactory.sizeConfigForCells(this._getOverlayCells()));
182252
}
183253

184254
private _maybeReturnFocusToCell(): void {

0 commit comments

Comments
 (0)