diff --git a/src/cdk-experimental/popover-edit/popover-edit-position-strategy-factory.ts b/src/cdk-experimental/popover-edit/popover-edit-position-strategy-factory.ts index b2ecf2baa88..a41f344eeaa 100644 --- a/src/cdk-experimental/popover-edit/popover-edit-position-strategy-factory.ts +++ b/src/cdk-experimental/popover-edit/popover-edit-position-strategy-factory.ts @@ -6,15 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, Injectable} from '@angular/core'; -import {Overlay, PositionStrategy} from '@angular/cdk/overlay'; +import {Directionality} from '@angular/cdk/bidi'; +import {Overlay, OverlaySizeConfig, PositionStrategy} from '@angular/cdk/overlay'; +import {Injectable} from '@angular/core'; /** - * Overridable factory responsible for configuring how cdkPopoverEdit popovers are positioned. + * Overridable factory responsible for configuring how cdkPopoverEdit popovers are positioned + * and sized. */ @Injectable() export abstract class PopoverEditPositionStrategyFactory { - abstract forElementRef(elementRef: ElementRef): PositionStrategy; + /** + * Creates a PositionStrategy based on the specified table cells. + * The cells will be provided in DOM order. + */ + abstract positionStrategyForCells(cells: HTMLElement[]): PositionStrategy; + + /** + * Creates an OverlaySizeConfig based on the specified table cells. + * The cells will be provided in DOM order. + */ + abstract sizeConfigForCells(cells: HTMLElement[]): OverlaySizeConfig; } /** @@ -24,14 +36,16 @@ export abstract class PopoverEditPositionStrategyFactory { */ @Injectable() export class DefaultPopoverEditPositionStrategyFactory extends PopoverEditPositionStrategyFactory { - constructor(protected readonly overlay: Overlay) { + constructor(protected readonly direction: Directionality, protected readonly overlay: Overlay) { super(); } - forElementRef(elementRef: ElementRef): PositionStrategy { - return this.overlay.position().flexibleConnectedTo(elementRef) + positionStrategyForCells(cells: HTMLElement[]): PositionStrategy { + return this.overlay.position() + .flexibleConnectedTo(cells[0]) .withGrowAfterOpen() .withPush() + .withViewportMargin(16) .withPositions([{ originX: 'start', originY: 'top', @@ -39,4 +53,25 @@ export class DefaultPopoverEditPositionStrategyFactory extends PopoverEditPositi overlayY: 'top', }]); } + + sizeConfigForCells(cells: HTMLElement[]): OverlaySizeConfig { + if (cells.length === 0) { + return {}; + } + + if (cells.length === 1) { + return {width: cells[0].getBoundingClientRect().width}; + } + + let firstCell, lastCell; + if (this.direction.value === 'ltr') { + firstCell = cells[0]; + lastCell = cells[cells.length - 1]; + } else { + lastCell = cells[0]; + firstCell = cells[cells.length - 1]; + } + + return {width: lastCell.getBoundingClientRect().right - firstCell.getBoundingClientRect().left}; + } } diff --git a/src/cdk-experimental/popover-edit/popover-edit.spec.ts b/src/cdk-experimental/popover-edit/popover-edit.spec.ts index 5f3aeb7341d..56a87af09c9 100644 --- a/src/cdk-experimental/popover-edit/popover-edit.spec.ts +++ b/src/cdk-experimental/popover-edit/popover-edit.spec.ts @@ -1,11 +1,12 @@ -import {BehaviorSubject} from 'rxjs'; -import {Component, ElementRef, Type, ViewChild} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {FormsModule, NgForm} from '@angular/forms'; -import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing'; import {DataSource} from '@angular/cdk/collections'; import {CdkTableModule} from '@angular/cdk/table'; -import {CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index'; +import {CommonModule} from '@angular/common'; +import {Component, ElementRef, Type, ViewChild} from '@angular/core'; +import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing'; +import {FormsModule, NgForm} from '@angular/forms'; +import {BehaviorSubject} from 'rxjs'; + +import {CdkPopoverEditColspan, CdkPopoverEditModule, PopoverEditClickOutBehavior} from './index'; const EDIT_TEMPLATE = `
@@ -33,6 +34,8 @@ const CELL_TEMPLATE = ` `; +const POPOVER_EDIT_DIRECTIVE = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`; + interface PeriodicElement { name: string; weight: number; @@ -45,6 +48,7 @@ abstract class BaseTestComponent { ignoreSubmitUnlessValid = true; clickOutBehavior: PopoverEditClickOutBehavior = 'close'; + colspan: CdkPopoverEditColspan = {}; onSubmit(element: PeriodicElement, form: NgForm) { if (!form.valid) { return; } @@ -62,7 +66,7 @@ abstract class BaseTestComponent { getEditCell(rowIndex = 0) { const row = getRows(this.table.nativeElement)[rowIndex]; - return getCells(row)[0]; + return getCells(row)[1]; } focusEditCell(rowIndex = 0) { @@ -84,6 +88,10 @@ abstract class BaseTestComponent { flush(); } + getEditPane() { + return document.querySelector('.cdk-edit-pane'); + } + getInput() { return document.querySelector('input') as HTMLInputElement|null; } @@ -125,7 +133,10 @@ abstract class BaseTestComponent { - + just a cell + + ${CELL_TEMPLATE} @@ -142,7 +153,9 @@ class VanillaTableOutOfCell extends BaseTestComponent { template: ` - + +
+ just a cell ${CELL_TEMPLATE} @@ -175,9 +188,15 @@ class ElementDataSource extends DataSource { template: `
+ + + just a cell + + + + ${POPOVER_EDIT_DIRECTIVE}> ${CELL_TEMPLATE} @@ -202,7 +221,7 @@ class ElementDataSource extends DataSource { ` }) class CdkFlexTableInCell extends BaseTestComponent { - displayedColumns = ['name', 'weight']; + displayedColumns = ['before', 'name', 'weight']; dataSource = new ElementDataSource(); } @@ -210,9 +229,15 @@ class CdkFlexTableInCell extends BaseTestComponent { template: `
+ + + +
+ just a cell + + ${POPOVER_EDIT_DIRECTIVE}> ${CELL_TEMPLATE} @@ -237,7 +262,7 @@ class CdkFlexTableInCell extends BaseTestComponent { ` }) class CdkTableInCell extends BaseTestComponent { - displayedColumns = ['name', 'weight']; + displayedColumns = ['before', 'name', 'weight']; dataSource = new ElementDataSource(); } @@ -316,6 +341,49 @@ describe('CDK Popover Edit', () => { expect(component.getInput()!.value).toBe('Hydrogen'); })); + it('positions the lens at the top left corner and spans the full width of the cell', + fakeAsync(() => { + component.openLens(); + + const paneRect = component.getEditPane()!.getBoundingClientRect(); + const cellRect = component.getEditCell().getBoundingClientRect(); + + expect(paneRect.width).toBe(cellRect.width); + expect(paneRect.left).toBe(cellRect.left); + expect(paneRect.top).toBe(cellRect.top); + })); + + it('adjusts the positioning of the lens based on colspan', fakeAsync(() => { + const cellRects = getCells(getRows(component.table.nativeElement)[0]) + .map(cell => cell.getBoundingClientRect()); + + component.colspan = {before: 1}; + fixture.detectChanges(); + + component.openLens(); + + let paneRect = component.getEditPane()!.getBoundingClientRect(); + expect(paneRect.top).toBe(cellRects[0].top); + expect(paneRect.left).toBe(cellRects[0].left); + expect(paneRect.right).toBe(cellRects[1].right); + + component.colspan = {after: 1}; + fixture.detectChanges(); + + paneRect = component.getEditPane()!.getBoundingClientRect(); + expect(paneRect.top).toBe(cellRects[1].top); + expect(paneRect.left).toBe(cellRects[1].left); + expect(paneRect.right).toBe(cellRects[2].right); + + component.colspan = {before: 1, after: 1}; + fixture.detectChanges(); + + paneRect = component.getEditPane()!.getBoundingClientRect(); + expect(paneRect.top).toBe(cellRects[0].top); + expect(paneRect.left).toBe(cellRects[0].left); + expect(paneRect.right).toBe(cellRects[2].right); + })); + it('updates the form and submits, closing the lens', fakeAsync(() => { component.openLens(); diff --git a/src/cdk-experimental/popover-edit/table-directives.ts b/src/cdk-experimental/popover-edit/table-directives.ts index f29395a22ee..6bce473490d 100644 --- a/src/cdk-experimental/popover-edit/table-directives.ts +++ b/src/cdk-experimental/popover-edit/table-directives.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y'; -import {Overlay, OverlayRef} from '@angular/cdk/overlay'; +import {Overlay, OverlayRef, PositionStrategy} from '@angular/cdk/overlay'; import {TemplatePortal} from '@angular/cdk/portal'; +import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; import { AfterViewInit, Directive, @@ -19,14 +20,24 @@ import { TemplateRef, ViewContainerRef } from '@angular/core'; -import {fromEvent, ReplaySubject} from 'rxjs'; -import {debounceTime, filter, map, mapTo, takeUntil} from 'rxjs/operators'; +import {fromEvent, merge, ReplaySubject} from 'rxjs'; +import {debounceTime, filter, map, mapTo, startWith, takeUntil} from 'rxjs/operators'; import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants'; import {EditEventDispatcher} from './edit-event-dispatcher'; import {closest} from './polyfill'; import {PopoverEditPositionStrategyFactory} from './popover-edit-position-strategy-factory'; +/** + * Describes the number of columns before and after the originating cell that the + * edit popup should span. In left to right locales, before means left and after means + * right. In right to left locales before means right and after means left. + */ +export interface CdkPopoverEditColspan { + before?: number; + after?: number; +} + /** * The delay between the mouse entering a row and the mouse stopping its movement before * showing on-hover content. @@ -111,6 +122,28 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { */ @Input('cdkPopoverEditContext') context?: C; + /** + * Specifies that the popup should cover additional table cells before and/or after + * this one. + */ + @Input('cdkPopoverEditColspan') + get colspan(): CdkPopoverEditColspan { + return this._colspan; + } + set colspan(value: CdkPopoverEditColspan) { + this._colspan = value; + + // Recompute positioning when the colspan changes. + if (this.overlayRef) { + this.overlayRef.updatePositionStrategy(this._getPositionStrategy()); + + if (this.overlayRef.hasAttached()) { + this._updateOverlaySize(); + } + } + } + private _colspan: CdkPopoverEditColspan = {}; + protected focusTrap?: FocusTrap; protected overlayRef?: OverlayRef; protected readonly destroyed = new ReplaySubject(); @@ -122,7 +155,9 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { protected readonly ngZone: NgZone, protected readonly overlay: Overlay, protected readonly positionFactory: PopoverEditPositionStrategyFactory, - protected readonly viewContainerRef: ViewContainerRef) {} + protected readonly scrollDispatcher: ScrollDispatcher, + protected readonly viewContainerRef: ViewContainerRef, + protected readonly viewportRuler: ViewportRuler) {} ngAfterViewInit(): void { this._startListeningToEditEvents(); @@ -161,8 +196,8 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { this.overlayRef = this.overlay.create({ disposeOnNavigation: true, panelClass: EDIT_PANE_CLASS, - positionStrategy: this.positionFactory.forElementRef(this.elementRef), - scrollStrategy: this.overlay.scrollStrategies.reposition({autoClose: true}), + positionStrategy: this._getPositionStrategy(), + scrollStrategy: this.overlay.scrollStrategies.reposition(), }); this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement); @@ -179,6 +214,41 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { this.viewContainerRef, {$implicit: this.context})); this.focusTrap!.focusInitialElementWhenReady(); + + // Update the size of the popup initially and on subsequent changes to + // scroll position and viewport size. + merge(this.scrollDispatcher.scrolled(), this.viewportRuler.change()) + .pipe( + startWith(null), + takeUntil(this.overlayRef!.detachments()), + takeUntil(this.destroyed), + ) + .subscribe(() => { + this._updateOverlaySize(); + }); + } + + private _getOverlayCells(): HTMLElement[] { + const cell = closest(this.elementRef.nativeElement!, CELL_SELECTOR) as HTMLElement; + + if (!this._colspan.before && !this._colspan.after) { + return [cell]; + } + + const row = closest(this.elementRef.nativeElement!, ROW_SELECTOR)!; + const rowCells = Array.from(row.querySelectorAll(CELL_SELECTOR)) as HTMLElement[]; + const ownIndex = rowCells.indexOf(cell); + + return rowCells.slice( + ownIndex - (this._colspan.before || 0), ownIndex + (this._colspan.after || 0) + 1); + } + + private _getPositionStrategy(): PositionStrategy { + return this.positionFactory.positionStrategyForCells(this._getOverlayCells()); + } + + private _updateOverlaySize(): void { + this.overlayRef!.updateSize(this.positionFactory.sizeConfigForCells(this._getOverlayCells())); } private _maybeReturnFocusToCell(): void { diff --git a/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.css b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.css new file mode 100644 index 00000000000..d19b3c187ac --- /dev/null +++ b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.css @@ -0,0 +1,23 @@ +.example-table { + width: 100%; +} + +.example-table th { + text-align: left; +} + +.example-table td, +.example-table th { + min-width: 300px; + width: 25%; +} + +.example-input-container { + display: flex; + justify-content: stretch; +} + +.example-input-container input { + flex: 1; + font-size: 14px; +} diff --git a/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.html b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.html new file mode 100644 index 00000000000..df7be8b086d --- /dev/null +++ b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.html @@ -0,0 +1,70 @@ + + + +
+
+
+ + + +
+
+
+ + + + +
+
+ + + + + + + + + + + + + + + + + +
No. First name Middle name Last name
{{person.id}} + {{person.firstName}} + + + + + + {{person.middleName}} + + + + + + {{person.lastName}} + + + + +
diff --git a/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.ts b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.ts new file mode 100644 index 00000000000..d419ab3c579 --- /dev/null +++ b/src/material-examples/cdk-popover-edit-cell-span-vanilla-table/cdk-popover-edit-cell-span-vanilla-table-example.ts @@ -0,0 +1,45 @@ +import {Component} from '@angular/core'; +import {NgForm} from '@angular/forms'; + +export interface Person { + id: number; + firstName: string; + middleName: string; + lastName: string; +} + +const PERSON_DATA: Person[] = [ + {id: 1, firstName: 'Terra', middleName: 'Maduin', lastName: 'Branford'}, + {id: 2, firstName: 'Locke', middleName: '', lastName: 'Cole'}, + {id: 3, firstName: 'Celes', middleName: 'Gestahl', lastName: 'Chere'}, + {id: 4, firstName: 'Edgar', middleName: 'Roni', lastName: 'Figaro'}, + {id: 5, firstName: 'Sabin', middleName: 'Rene', lastName: 'Figaro'}, + {id: 6, firstName: 'Clyde', middleName: '"Shadow"', lastName: 'Arrowny'}, + {id: 7, firstName: 'Setzer', middleName: '', lastName: 'Gabbiani'}, + {id: 8, firstName: 'Cid', middleName: 'Del Norte', lastName: 'Marquez'}, + {id: 9, firstName: 'Mog', middleName: '', lastName: 'McMoogle'}, +]; + +/** + * @title CDK Popover Edit spanning multiple columns on an HTML data-table + */ +@Component({ + selector: 'cdk-popover-edit-cell-span-vanilla-table-example', + styleUrls: ['cdk-popover-edit-cell-span-vanilla-table-example.css'], + templateUrl: 'cdk-popover-edit-cell-span-vanilla-table-example.html', +}) +export class CdkPopoverEditCellSpanVanillaTableExample { + readonly preservedValues = new WeakMap(); + + readonly persons = PERSON_DATA; + + onSubmit(person: Person, f: NgForm) { + if (!f.valid) { + return; + } + + person.firstName = f.value['firstName']; + person.middleName = f.value['middleName']; + person.lastName = f.value['lastName']; + } +} diff --git a/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.css b/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.css index eba60b11b33..adb5aa2786f 100644 --- a/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.css +++ b/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.css @@ -8,5 +8,6 @@ .example-table td, .example-table th { + min-width: 300px; width: 25%; } diff --git a/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.html b/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.html index fa31d52a25c..fe4d4dbd791 100644 --- a/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.html +++ b/src/material-examples/cdk-popover-edit-vanilla-table/cdk-popover-edit-vanilla-table-example.html @@ -4,7 +4,7 @@ its context. It could be reused in multiple cells. --> -
+
-
+