mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(popover-edit): allow tabbing from popup to next/previous cell (#15793)
This commit is contained in:
committed by
Jeremy Elbourn
parent
82b56793f8
commit
d3d026b42d
@@ -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<Element|null>();
|
||||
|
||||
/** The EditRef for the currently active edit lens (if any). */
|
||||
get editRef(): EditRef<any>|null {
|
||||
return this._editRef;
|
||||
}
|
||||
private _editRef: EditRef<any>|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<any>) {
|
||||
this._editRef = ref;
|
||||
}
|
||||
|
||||
/** Unsets the currently active EditRef, if the specified editRef is active. */
|
||||
unsetActiveEditRef(ref: EditRef<any>) {
|
||||
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
|
||||
|
||||
@@ -23,6 +23,10 @@ export class EditRef<FormValue> implements OnDestroy {
|
||||
private readonly _finalValueSubject = new Subject<FormValue>();
|
||||
readonly finalValue: Observable<FormValue> = this._finalValueSubject.asObservable();
|
||||
|
||||
/** Emits when the user tabs out of this edit lens before closing. */
|
||||
private readonly _blurredSubject = new Subject<void>();
|
||||
readonly blurred: Observable<void> = this._blurredSubject.asObservable();
|
||||
|
||||
/** The value to set the form back to on revert. */
|
||||
private _revertFormValue: FormValue;
|
||||
|
||||
@@ -37,7 +41,9 @@ export class EditRef<FormValue> 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<FormValue> implements OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._editEventDispatcher.unsetActiveEditRef(this);
|
||||
this._finalValueSubject.next(this._form.value);
|
||||
this._finalValueSubject.complete();
|
||||
}
|
||||
@@ -76,6 +83,11 @@ export class EditRef<FormValue> 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
|
||||
|
||||
@@ -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<FocusEscapeNotifierDirection>();
|
||||
|
||||
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<FocusEscapeNotifierDirection> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ export class CdkEditControl<FormValue> 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<FormValue> 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<FormValue> 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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,7 +28,13 @@ const NAME_EDIT_TEMPLATE = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
const WEIGHT_EDIT_TEMPLATE = `<div> Just a placeholder </div>`;
|
||||
const WEIGHT_EDIT_TEMPLATE = `
|
||||
<div>
|
||||
<form #f="ngForm" cdkEditControl>
|
||||
<input>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<C> 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<C> 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<C> 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<C> extends CdkPopoverEdit<C> {
|
||||
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.
|
||||
|
||||
+13
@@ -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%;
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<table editable class="example-table">
|
||||
<!--
|
||||
This edit lens is specified outside of the cell and must explicitly declare
|
||||
its context. It could be reused in multiple cells.
|
||||
-->
|
||||
<ng-template #weightEdit let-element>
|
||||
<div style="background-color: white; width: 100%">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
cdkEditControlClickOutBehavior="submit"
|
||||
(ngSubmit)="onSubmitWeight(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedWeightValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedWeightValues.set(element, $event)">
|
||||
<input type="number" [ngModel]="element.weight" name="weight" required>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<tr>
|
||||
<th> No. </th>
|
||||
<th> Name </th>
|
||||
<th> Weight </th>
|
||||
<th> Symbol </th>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let element of elements">
|
||||
<td> {{element.position}} </td>
|
||||
|
||||
<td [cdkPopoverEdit]="nameEdit" cdkPopoverEditTabOut cdkEditOpen>
|
||||
{{element.name}}
|
||||
|
||||
<!-- This edit is defined in the cell and can implicitly access element -->
|
||||
<ng-template #nameEdit>
|
||||
<div style="background-color: white; width: 100%">
|
||||
<form #f="ngForm"
|
||||
cdkEditControl
|
||||
cdkEditControlClickOutBehavior="submit"
|
||||
(ngSubmit)="onSubmitName(element, f)"
|
||||
[cdkEditControlPreservedFormValue]="preservedNameValues.get(element)"
|
||||
(cdkEditControlPreservedFormValueChange)="preservedNameValues.set(element, $event)">
|
||||
<input [ngModel]="element.name" name="name" required>
|
||||
<br>
|
||||
<button type="submit">Confirm</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
|
||||
<td [cdkPopoverEdit]="weightEdit" [cdkPopoverEditContext]="element"
|
||||
cdkPopoverEditTabOut cdkEditOpen>
|
||||
{{element.weight}}
|
||||
</td>
|
||||
|
||||
<td> {{element.symbol}} </td>
|
||||
</tr>
|
||||
</table>
|
||||
+59
@@ -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<PeriodicElement, any>();
|
||||
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user