From d3d026b42d7ea47bdf42360664cfc4680e516579 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Tue, 16 Apr 2019 00:47:24 -0400 Subject: [PATCH] feat(popover-edit): allow tabbing from popup to next/previous cell (#15793) --- .../popover-edit/edit-event-dispatcher.ts | 21 +++ src/cdk-experimental/popover-edit/edit-ref.ts | 14 +- .../popover-edit/focus-escape-notifier.ts | 74 ++++++++ .../popover-edit/lens-directives.ts | 15 +- .../popover-edit/popover-edit-module.ts | 2 + .../popover-edit/popover-edit.spec.ts | 174 ++++++++++++------ .../popover-edit/table-directives.ts | 78 +++++++- ...ver-edit-tab-out-vanilla-table-example.css | 13 ++ ...er-edit-tab-out-vanilla-table-example.html | 56 ++++++ ...over-edit-tab-out-vanilla-table-example.ts | 59 ++++++ 10 files changed, 445 insertions(+), 61 deletions(-) create mode 100644 src/cdk-experimental/popover-edit/focus-escape-notifier.ts create mode 100644 src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.css create mode 100644 src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.html create mode 100644 src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.ts diff --git a/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts b/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts index 37de5d7ecc3..acfa60e2ab4 100644 --- a/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts +++ b/src/cdk-experimental/popover-edit/edit-event-dispatcher.ts @@ -12,6 +12,7 @@ import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators'; import {CELL_SELECTOR, ROW_SELECTOR} from './constants'; import {closest} from './polyfill'; +import {EditRef} from './edit-ref'; /** The delay between mouse out events and hiding hover content. */ const DEFAULT_MOUSE_OUT_DELAY_MS = 30; @@ -30,6 +31,12 @@ export class EditEventDispatcher { /** A subject that emits mouse move events for table rows. */ readonly mouseMove = new Subject(); + /** The EditRef for the currently active edit lens (if any). */ + get editRef(): EditRef|null { + return this._editRef; + } + private _editRef: EditRef|null = null; + /** The table cell that has an active edit lens (or null). */ private _currentlyEditing: Element|null = null; @@ -67,6 +74,20 @@ export class EditEventDispatcher { } } + /** Sets the currently active EditRef. */ + setActiveEditRef(ref: EditRef) { + this._editRef = ref; + } + + /** Unsets the currently active EditRef, if the specified editRef is active. */ + unsetActiveEditRef(ref: EditRef) { + if (this._editRef !== ref) { + return; + } + + this._editRef = null; + } + /** * Gets an Observable that emits true when the specified element's row * is being hovered over and false when not. Hovering is defined as when diff --git a/src/cdk-experimental/popover-edit/edit-ref.ts b/src/cdk-experimental/popover-edit/edit-ref.ts index e269eb869de..01f5d4e1fbb 100644 --- a/src/cdk-experimental/popover-edit/edit-ref.ts +++ b/src/cdk-experimental/popover-edit/edit-ref.ts @@ -23,6 +23,10 @@ export class EditRef implements OnDestroy { private readonly _finalValueSubject = new Subject(); readonly finalValue: Observable = this._finalValueSubject.asObservable(); + /** Emits when the user tabs out of this edit lens before closing. */ + private readonly _blurredSubject = new Subject(); + readonly blurred: Observable = this._blurredSubject.asObservable(); + /** The value to set the form back to on revert. */ private _revertFormValue: FormValue; @@ -37,7 +41,9 @@ export class EditRef implements OnDestroy { constructor( @Self() private readonly _form: ControlContainer, - private readonly _editEventDispatcher: EditEventDispatcher) {} + private readonly _editEventDispatcher: EditEventDispatcher) { + this._editEventDispatcher.setActiveEditRef(this); + } /** * Called by the host directive's OnInit hook. Reads the initial state of the @@ -57,6 +63,7 @@ export class EditRef implements OnDestroy { } ngOnDestroy(): void { + this._editEventDispatcher.unsetActiveEditRef(this); this._finalValueSubject.next(this._form.value); this._finalValueSubject.complete(); } @@ -76,6 +83,11 @@ export class EditRef implements OnDestroy { this._editEventDispatcher.editing.next(null); } + /** Notifies the active edit that the user has moved focus out of the lens. */ + blur(): void { + this._blurredSubject.next(); + } + /** * Closes the edit if the enter key is not down. * Otherwise, sets _closePending to true so that the edit will close on the diff --git a/src/cdk-experimental/popover-edit/focus-escape-notifier.ts b/src/cdk-experimental/popover-edit/focus-escape-notifier.ts new file mode 100644 index 00000000000..ad1424ca039 --- /dev/null +++ b/src/cdk-experimental/popover-edit/focus-escape-notifier.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, NgZone} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {FocusTrap, InteractivityChecker} from '@angular/cdk/a11y'; +import {Observable, Subject} from 'rxjs'; + +/** Value indicating whether focus left the target area before or after the enclosed elements. */ +export const enum FocusEscapeNotifierDirection { + START, + END, +} + +/** + * Like FocusTrap, but rather than trapping focus within a dom region, notifies subscribers when + * focus leaves the region. + */ +export class FocusEscapeNotifier extends FocusTrap { + private _escapeSubject = new Subject(); + + constructor( + element: HTMLElement, + checker: InteractivityChecker, + ngZone: NgZone, + document: Document) { + super(element, checker, ngZone, document, true /* deferAnchors */); + + // The focus trap adds "anchors" at the beginning and end of a trapped region that redirect + // focus. We override that redirect behavior here with simply emitting on a stream. + this.startAnchorListener = () => { + this._escapeSubject.next(FocusEscapeNotifierDirection.START); + return true; + }; + this.endAnchorListener = () => { + this._escapeSubject.next(FocusEscapeNotifierDirection.END); + return true; + }; + + this.attachAnchors(); + } + + escapes(): Observable { + return this._escapeSubject.asObservable(); + } +} + +/** Factory that allows easy instantiation of focus escape notifiers. */ +@Injectable({providedIn: 'root'}) +export class FocusEscapeNotifierFactory { + private _document: Document; + + constructor( + private _checker: InteractivityChecker, + private _ngZone: NgZone, + @Inject(DOCUMENT) _document: any) { + + this._document = _document; + } + + /** + * Creates a focus escape notifier region around the given element. + * @param element The element around which focus will be monitored. + * @returns The created focus escape notifier instance. + */ + create(element: HTMLElement): FocusEscapeNotifier { + return new FocusEscapeNotifier(element, this._checker, this._ngZone, this._document); + } +} diff --git a/src/cdk-experimental/popover-edit/lens-directives.ts b/src/cdk-experimental/popover-edit/lens-directives.ts index c7691c48d8b..28c372a7723 100644 --- a/src/cdk-experimental/popover-edit/lens-directives.ts +++ b/src/cdk-experimental/popover-edit/lens-directives.ts @@ -61,6 +61,7 @@ export class CdkEditControl implements OnDestroy, OnInit { ngOnInit(): void { this.editRef.init(this.preservedFormValue); this.editRef.finalValue.subscribe(this.preservedFormValueChange); + this.editRef.blurred.subscribe(() => this._handleBlur()); } ngOnDestroy(): void { @@ -97,7 +98,7 @@ export class CdkEditControl implements OnDestroy, OnInit { switch (this.clickOutBehavior) { case 'submit': // Manually cause the form to submit before closing. - this.elementRef.nativeElement!.dispatchEvent(new Event('submit')); + this._triggerFormSubmit(); // Fall through case 'close': this.editRef.close(); @@ -106,6 +107,18 @@ export class CdkEditControl implements OnDestroy, OnInit { break; } } + + /** Triggers submit on tab out if clickOutBehavior is 'submit'. */ + private _handleBlur(): void { + if (this.clickOutBehavior === 'submit') { + // Manually cause the form to submit before closing. + this._triggerFormSubmit(); + } + } + + private _triggerFormSubmit() { + this.elementRef.nativeElement!.dispatchEvent(new Event('submit')); + } } /** Reverts the form to its initial or previously submitted state on click. */ diff --git a/src/cdk-experimental/popover-edit/popover-edit-module.ts b/src/cdk-experimental/popover-edit/popover-edit-module.ts index cbcf013408a..44e9bd03cea 100644 --- a/src/cdk-experimental/popover-edit/popover-edit-module.ts +++ b/src/cdk-experimental/popover-edit/popover-edit-module.ts @@ -10,6 +10,7 @@ import {NgModule} from '@angular/core'; import {OverlayModule} from '@angular/cdk/overlay'; import { CdkPopoverEdit, + CdkPopoverEditTabOut, CdkRowHoverContent, CdkEditable, CdkEditOpen, @@ -26,6 +27,7 @@ import { const EXPORTED_DECLARATIONS = [ CdkPopoverEdit, + CdkPopoverEditTabOut, CdkRowHoverContent, CdkEditControl, CdkEditRevert, diff --git a/src/cdk-experimental/popover-edit/popover-edit.spec.ts b/src/cdk-experimental/popover-edit/popover-edit.spec.ts index c8fe8016b60..5e1062c3d02 100644 --- a/src/cdk-experimental/popover-edit/popover-edit.spec.ts +++ b/src/cdk-experimental/popover-edit/popover-edit.spec.ts @@ -28,7 +28,13 @@ const NAME_EDIT_TEMPLATE = ` `; -const WEIGHT_EDIT_TEMPLATE = `
Just a placeholder
`; +const WEIGHT_EDIT_TEMPLATE = ` +
+
+ +
+
+ `; const CELL_TEMPLATE = ` {{element.name}} @@ -40,7 +46,7 @@ const CELL_TEMPLATE = ` const POPOVER_EDIT_DIRECTIVE_NAME = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`; -const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit"`; +const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit" cdkPopoverEditTabOut`; interface PeriodicElement { name: string; @@ -74,13 +80,13 @@ abstract class BaseTestComponent { return getRows(this.table.nativeElement); } - getEditCell(rowIndex = 0) { + getEditCell(rowIndex = 0, cellIndex = 1) { const row = this.getRows()[rowIndex]; - return getCells(row)[1]; + return getCells(row)[cellIndex]; } - focusEditCell(rowIndex = 0) { - this.getEditCell(rowIndex).focus(); + focusEditCell(rowIndex = 0, cellIndex = 1) { + this.getEditCell(rowIndex, cellIndex).focus(); } getOpenButton(rowIndex = 0) { @@ -91,10 +97,10 @@ abstract class BaseTestComponent { this.getOpenButton(rowIndex)!.click(); } - openLens(rowIndex = 0) { - this.focusEditCell(rowIndex); - this.getEditCell(rowIndex).dispatchEvent( - new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'})); + openLens(rowIndex = 0, cellIndex = 1) { + this.focusEditCell(rowIndex, cellIndex); + this.getEditCell(rowIndex, cellIndex) + .dispatchEvent(new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'})); flush(); } @@ -102,6 +108,10 @@ abstract class BaseTestComponent { return document.querySelector('.cdk-edit-pane'); } + getEditBoundingBox() { + return document.querySelector('.cdk-overlay-connected-position-bounding-box'); + } + getInput() { return document.querySelector('input') as HTMLInputElement|null; } @@ -366,68 +376,124 @@ describe('CDK Popover Edit', () => { })); }); - describe('arrow key focus manipulation', () => { - const dispatchKey = (cell: HTMLElement, keyCode: number) => - dispatchKeyboardEvent(cell, 'keydown', keyCode, cell); - + describe('focus manipulation', () => { const getRowCells = () => component.getRows().map(getCells); - it('moves focus up/down/left/right and prevents default', () => { - const rowCells = getRowCells(); + describe('arrow keys', () => { + const dispatchKey = (cell: HTMLElement, keyCode: number) => + dispatchKeyboardEvent(cell, 'keydown', keyCode, cell); - // Focus the upper-left editable cell. - rowCells[0][1].focus(); + it('moves focus up/down/left/right and prevents default', () => { + const rowCells = getRowCells(); - const downEvent = dispatchKey(rowCells[0][1], DOWN_ARROW); - expect(document.activeElement).toBe(rowCells[1][1]); - expect(downEvent.defaultPrevented).toBe(true); + // Focus the upper-left editable cell. + rowCells[0][1].focus(); - const rightEvent = dispatchKey(rowCells[1][1], RIGHT_ARROW); - expect(document.activeElement).toBe(rowCells[1][2]); - expect(rightEvent.defaultPrevented).toBe(true); + const downEvent = dispatchKey(rowCells[0][1], DOWN_ARROW); + expect(document.activeElement).toBe(rowCells[1][1]); + expect(downEvent.defaultPrevented).toBe(true); - const upEvent = dispatchKey(rowCells[1][2], UP_ARROW); - expect(document.activeElement).toBe(rowCells[0][2]); - expect(upEvent.defaultPrevented).toBe(true); + const rightEvent = dispatchKey(rowCells[1][1], RIGHT_ARROW); + expect(document.activeElement).toBe(rowCells[1][2]); + expect(rightEvent.defaultPrevented).toBe(true); - const leftEvent = dispatchKey(rowCells[0][2], LEFT_ARROW); - expect(document.activeElement).toBe(rowCells[0][1]); - expect(leftEvent.defaultPrevented).toBe(true); + const upEvent = dispatchKey(rowCells[1][2], UP_ARROW); + expect(document.activeElement).toBe(rowCells[0][2]); + expect(upEvent.defaultPrevented).toBe(true); + + const leftEvent = dispatchKey(rowCells[0][2], LEFT_ARROW); + expect(document.activeElement).toBe(rowCells[0][1]); + expect(leftEvent.defaultPrevented).toBe(true); + }); + + it('wraps around when reaching start or end of a row, skipping non-editable cells', + () => { + const rowCells = getRowCells(); + + // Focus the upper-right editable cell. + rowCells[0][2].focus(); + + dispatchKey(rowCells[0][2], RIGHT_ARROW); + expect(document.activeElement).toBe(rowCells[1][1]); + + dispatchKey(rowCells[1][1], LEFT_ARROW); + expect(document.activeElement).toBe(rowCells[0][2]); + }); + + it('does not fall off top or bottom of the table', () => { + const rowCells = getRowCells(); + + // Focus the upper-left editable cell. + rowCells[0][1].focus(); + + dispatchKey(rowCells[0][1], UP_ARROW); + expect(document.activeElement).toBe(rowCells[0][1]); + + // Focus the bottom-left editable cell. + rowCells[4][1].focus(); + dispatchKey(rowCells[4][1], DOWN_ARROW); + expect(document.activeElement).toBe(rowCells[4][1]); + }); + + it('ignores non arrow key events', () => { + component.focusEditCell(); + const cell = component.getEditCell(); + + expect(dispatchKey(cell, TAB).defaultPrevented).toBe(false); + }); }); - it('wraps around when reaching start or end of a row, skipping non-editable cells', () => { - const rowCells = getRowCells(); + describe('lens focus trapping behavior', () => { + const getFocusablePaneElements = () => + Array.from(component.getEditBoundingBox()!.querySelectorAll( + 'input, button, .cdk-focus-trap-anchor')) as HTMLElement[]; - // Focus the upper-right editable cell. - rowCells[0][2].focus(); + it('keeps focus within the lens by default', fakeAsync(() => { + // Open the name lens which has the default behavior. + component.openLens(); - dispatchKey(rowCells[0][2], RIGHT_ARROW); - expect(document.activeElement).toBe(rowCells[1][1]); + const focusableElements = getFocusablePaneElements(); - dispatchKey(rowCells[1][1], LEFT_ARROW); - expect(document.activeElement).toBe(rowCells[0][2]); - }); + // Focus the last element (end focus trap anchor). + focusableElements[focusableElements.length - 1].focus(); + flush(); - it('does not fall off top or bottom of the table', () => { - const rowCells = getRowCells(); + // Focus should have moved to the top of the lens. + expect(document.activeElement).toBe(focusableElements[1]); + expect(component.lensIsOpen()).toBe(true); + })); - // Focus the upper-left editable cell. - rowCells[0][1].focus(); + it('moves focus to the next cell when focus leaves end of lens with cdkPopoverEditTabOut', + fakeAsync(() => { + // Open the weight lens which has tab out behavior. + component.openLens(0, 2); - dispatchKey(rowCells[0][1], UP_ARROW); - expect(document.activeElement).toBe(rowCells[0][1]); + const focusableElements = getFocusablePaneElements(); - // Focus the bottom-left editable cell. - rowCells[4][1].focus(); - dispatchKey(rowCells[4][1], DOWN_ARROW); - expect(document.activeElement).toBe(rowCells[4][1]); - }); + // Focus the last element (end focus trap anchor). + focusableElements[focusableElements.length - 1].focus(); + flush(); - it('ignores non arrow key events', () => { - component.focusEditCell(); - const cell = component.getEditCell(); + // Focus should have moved to the next editable cell. + expect(document.activeElement).toBe(component.getEditCell(1, 1)); + expect(component.lensIsOpen()).toBe(false); + })); - expect(dispatchKey(cell, TAB).defaultPrevented).toBe(false); + it(`moves focus to the previous cell when focus leaves end of lens with +cdkPopoverEditTabOut`, fakeAsync(() => { + // Open the weight lens which has tab out behavior. + component.openLens(0, 2); + + const focusableElements = getFocusablePaneElements(); + + // Focus the first (start focus trap anchor). + focusableElements[0].focus(); + flush(); + + // Focus should have moved to the next editable cell. + expect(document.activeElement).toBe(component.getEditCell(0, 1)); + expect(component.lensIsOpen()).toBe(false); + })); }); }); diff --git a/src/cdk-experimental/popover-edit/table-directives.ts b/src/cdk-experimental/popover-edit/table-directives.ts index 9f65be74f30..af493fb5cd6 100644 --- a/src/cdk-experimental/popover-edit/table-directives.ts +++ b/src/cdk-experimental/popover-edit/table-directives.ts @@ -26,6 +26,11 @@ import {debounceTime, filter, map, mapTo, startWith, takeUntil} from 'rxjs/opera import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants'; import {EditEventDispatcher} from './edit-event-dispatcher'; import {FocusDispatcher} from './focus-dispatcher'; +import { + FocusEscapeNotifier, + FocusEscapeNotifierDirection, + FocusEscapeNotifierFactory +} from './focus-escape-notifier'; import {closest} from './polyfill'; import {PopoverEditPositionStrategyFactory} from './popover-edit-position-strategy-factory'; @@ -112,7 +117,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy { * Makes the cell focusable. */ @Directive({ - selector: '[cdkPopoverEdit]', + selector: '[cdkPopoverEdit]:not([cdkPopoverEditTabOut])', host: { 'tabIndex': '0', 'class': 'cdk-popover-edit-cell', @@ -179,6 +184,14 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { } } + protected initFocusTrap(): void { + this.focusTrap = this.focusTrapFactory.create(this.overlayRef!.overlayElement); + } + + protected closeEditOverlay(): void { + this.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!); + } + private _startListeningToEditEvents(): void { this.editEventDispatcher.editingCell(this.elementRef.nativeElement!) .pipe(takeUntil(this.destroyed)) @@ -207,12 +220,10 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { scrollStrategy: this.overlay.scrollStrategies.reposition(), }); - this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement); + this.initFocusTrap(); this.overlayRef.overlayElement.setAttribute('aria-role', 'dialog'); - this.overlayRef.detachments().subscribe(() => { - this.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!); - }); + this.overlayRef.detachments().subscribe(() => this.closeEditOverlay()); } private _showEditOverlay(): void { @@ -266,6 +277,63 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { } } +/** + * Attaches an ng-template to a cell and shows it when instructed to by the + * EditEventDispatcher service. + * Makes the cell focusable. + */ +@Directive({ + selector: '[cdkPopoverEdit] [cdkPopoverEditTabOut]', + host: { + 'tabIndex': '0', + 'class': 'cdk-popover-edit-cell', + '[attr.aria-haspopup]': 'true', + } +}) +export class CdkPopoverEditTabOut extends CdkPopoverEdit { + protected focusTrap?: FocusEscapeNotifier; + + constructor( + editEventDispatcher: EditEventDispatcher, + elementRef: ElementRef, + focusTrapFactory: FocusTrapFactory, + ngZone: NgZone, + overlay: Overlay, + positionFactory: PopoverEditPositionStrategyFactory, + scrollDispatcher: ScrollDispatcher, + viewContainerRef: ViewContainerRef, + viewportRuler: ViewportRuler, + protected readonly focusEscapeNotifierFactory: FocusEscapeNotifierFactory, + protected readonly focusDispatcher: FocusDispatcher) { + super( + editEventDispatcher, + elementRef, + focusTrapFactory, + ngZone, + overlay, + positionFactory, + scrollDispatcher, + viewContainerRef, + viewportRuler); + } + + protected initFocusTrap(): void { + this.focusTrap = this.focusEscapeNotifierFactory.create(this.overlayRef!.overlayElement); + + this.focusTrap.escapes().pipe(takeUntil(this.destroyed)).subscribe(direction => { + if (this.editEventDispatcher.editRef) { + this.editEventDispatcher.editRef.blur(); + } + + this.focusDispatcher.moveFocusHorizontally( + closest(this.elementRef.nativeElement!, CELL_SELECTOR) as HTMLElement, + direction === FocusEscapeNotifierDirection.START ? -1 : 1); + + this.closeEditOverlay(); + }); + } +} + /** * A structural directive that shows its contents when the table row containing * it is hovered. diff --git a/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.css b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.css new file mode 100644 index 00000000000..adb5aa2786f --- /dev/null +++ b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.css @@ -0,0 +1,13 @@ +.example-table { + width: 100%; +} + +.example-table th { + text-align: left; +} + +.example-table td, +.example-table th { + min-width: 300px; + width: 25%; +} diff --git a/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.html b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.html new file mode 100644 index 00000000000..13270f0a01f --- /dev/null +++ b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.html @@ -0,0 +1,56 @@ + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
No. Name Weight Symbol
{{element.position}} + {{element.name}} + + + +
+
+ +
+ +
+
+
+
+ {{element.weight}} + {{element.symbol}}
diff --git a/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.ts b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.ts new file mode 100644 index 00000000000..3b49e1134e2 --- /dev/null +++ b/src/material-examples/cdk-popover-edit-tab-out-vanilla-table/cdk-popover-edit-tab-out-vanilla-table-example.ts @@ -0,0 +1,59 @@ +import {Component} from '@angular/core'; +import {NgForm} from '@angular/forms'; + +export interface PeriodicElement { + name: string; + position: number; + weight: number; + symbol: string; +} + +const ELEMENT_DATA: PeriodicElement[] = [ + {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'}, + {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}, + {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'}, + {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'}, + {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'}, + {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'}, + {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'}, + {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'}, + {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'}, + {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'}, + {position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'}, + {position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'}, + {position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'}, + {position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'}, + {position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'}, + {position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'}, + {position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'}, + {position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'}, + {position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'}, + {position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'}, +]; + +/** + * @title CDK Popover Edit with spreadsheet-like configuration on an HTML data-table + */ +@Component({ + selector: 'cdk-popover-edit-tab-out-vanilla-table-example', + styleUrls: ['cdk-popover-edit-tab-out-vanilla-table-example.css'], + templateUrl: 'cdk-popover-edit-tab-out-vanilla-table-example.html', +}) +export class CdkPopoverEditTabOutVanillaTableExample { + readonly preservedNameValues = new WeakMap(); + readonly preservedWeightValues = new WeakMap(); + + readonly elements = ELEMENT_DATA; + + onSubmitName(element: PeriodicElement, f: NgForm) { + if (!f.valid) { return; } + + element.name = f.value.name; + } + + onSubmitWeight(element: PeriodicElement, f: NgForm) { + if (!f.valid) { return; } + + element.weight = f.value.weight; + } +}