Skip to content

Commit

Permalink
feat(popover-edit): experimental popover edit for tables (mvp)
Browse files Browse the repository at this point in the history
  • Loading branch information
kseamon committed Apr 1, 2019
1 parent bd66e5c commit 16ce33a
Show file tree
Hide file tree
Showing 33 changed files with 1,915 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -89,6 +89,7 @@
/src/cdk-experimental/** @jelbourn
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
/src/cdk-experimental/scrolling/** @mmalerba
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin

# Docs examples & guides
/guides/** @jelbourn
Expand Down Expand Up @@ -130,6 +131,7 @@
/src/dev-app/paginator/** @andrewseguin
/src/dev-app/platform/** @jelbourn @devversion
/src/dev-app/portal/** @jelbourn
/src/dev-app/popover-edit/** @kseamon @andrewseguin
/src/dev-app/progress-bar/** @jelbourn @crisbeto @josephperrott
/src/dev-app/progress-spinner/** @jelbourn @crisbeto @josephperrott
/src/dev-app/radio/** @jelbourn @devversion
Expand Down
1 change: 1 addition & 0 deletions packages.bzl
Expand Up @@ -24,6 +24,7 @@ CDK_TARGETS = ["//src/cdk"] + ["//src/cdk/%s" % p for p in CDK_PACKAGES]

CDK_EXPERIMENTAL_PACKAGES = [
"dialog",
"popover-edit",
"scrolling",
]

Expand Down
36 changes: 36 additions & 0 deletions src/cdk-experimental/popover-edit/BUILD.bazel
@@ -0,0 +1,36 @@
package(default_visibility=["//visibility:public"])

load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

ng_module(
name = "popover-edit",
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/cdk-experimental/popover-edit",
deps = [
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
"@npm//rxjs",
"//src/cdk/a11y",
"//src/cdk/overlay",
"//src/cdk/portal",
],
)

ng_test_library(
name = "popover_edit_test_sources",
srcs = glob(["**/*.spec.ts"]),
deps = [
":popover-edit",
"@npm//@angular/common",
"@npm//@angular/forms",
"@npm//rxjs",
"//src/cdk/collections",
"//src/cdk/table",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":popover_edit_test_sources"]
)
19 changes: 19 additions & 0 deletions src/cdk-experimental/popover-edit/constants.ts
@@ -0,0 +1,19 @@
/**
* @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
*/

/** Selector for finding table cells. */
export const CELL_SELECTOR = '.cdk-cell, .mat-cell, td';

/** Selector for finding table rows. */
export const ROW_SELECTOR = '.cdk-row, .mat-row, tr';

/** CSS class added to the edit lens pane. */
export const EDIT_PANE_CLASS = 'cdk-edit-pane';

/** Selector for finding the edit lens pane. */
export const EDIT_PANE_SELECTOR = '.' + EDIT_PANE_CLASS;
86 changes: 86 additions & 0 deletions src/cdk-experimental/popover-edit/edit-event-dispatcher.ts
@@ -0,0 +1,86 @@
/**
* @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 {Injectable} from '@angular/core';
import {Observable, Subject, timer} from 'rxjs';
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';

import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
import {closest} from './polyfill';

/** The delay between mouse out events and hiding hover content. */
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;

/**
* Service for sharing delegated events and state for triggering table edits.
*/
@Injectable()
export class EditEventDispatcher {
/** A subject that indicates which table cell is currently editing. */
readonly editing = new Subject<Element|null>();

/** A subject that indicates which table row is currently hovered. */
readonly hovering = new Subject<Element|null>();

/** A subject that emits mouse move events for table rows. */
readonly mouseMove = new Subject<Element|null>();

/** The table cell that has an active edit lens (or null). */
private _currentlyEditing: Element|null = null;

private readonly _hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
private readonly _editingDistinct = this.editing.pipe(distinctUntilChanged(), share());

constructor() {
this._editingDistinct.subscribe(cell => {
this._currentlyEditing = cell;
});
}

/**
* Gets an Observable that emits true when the specified element's cell
* is editing and false when not.
*/
editingCell(element: Element|EventTarget): Observable<boolean> {
let cell: Element|null = null;

return this._editingDistinct.pipe(
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
distinctUntilChanged(),
);
}

/**
* Stops editing for the specified cell. If the specified cell is not the current
* edit cell, does nothing.
*/
doneEditingCell(element: Element|EventTarget): void {
const cell = closest(element, CELL_SELECTOR);

if (this._currentlyEditing === cell) {
this.editing.next(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
* the mouse has momentarily stopped moving over the cell.
*/
hoveringOnRow(element: Element|EventTarget): Observable<boolean> {
let row: Element|null = null;

return this._hoveringDistinct.pipe(
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
audit(
(hovering) => hovering ? this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
timer(DEFAULT_MOUSE_OUT_DELAY_MS)),
distinctUntilChanged(),
);
}
}
114 changes: 114 additions & 0 deletions src/cdk-experimental/popover-edit/edit-ref.ts
@@ -0,0 +1,114 @@
/**
* @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 {Injectable, OnDestroy, Self} from '@angular/core';
import {ControlContainer} from '@angular/forms';
import {Subject} from 'rxjs';
import {take} from 'rxjs/operators';

import {EditEventDispatcher} from './edit-event-dispatcher';

/**
* Used for communication between the form within the edit lens and the
* table that launched it. Provided by CdkEditControl within the lens.
*/
@Injectable()
export class EditRef<FormValue> implements OnDestroy {
/** Emits the final value of this edit instance before closing. */
private readonly _finalValueSubject = new Subject<FormValue>();
readonly finalValue = this._finalValueSubject.asObservable();

/** The value to set the form back to on revert. */
private _revertFormValue: FormValue;

/**
* The flags are used to track whether a keyboard enter press is in progress at the same time
* as other events that would cause the edit lens to close. We must track this so that the
* Enter keyup event does not fire after we close as it would cause the edit to immediately
* reopen.
*/
private _enterPressed = false;
private _closePending = false;

constructor(
@Self() private readonly _form: ControlContainer,
private readonly _editEventDispatcher: EditEventDispatcher) {}

/**
* Called by the host directive's OnInit hook. Reads the initial state of the
* form and overrides it with persisted state from previous openings, if
* applicable.
*/
init(previousFormValue: FormValue|undefined): void {
// Wait for either the first value to be set, then override it with
// the previously entered value, if any.
this._form.valueChanges!.pipe(take(1)).subscribe(() => {
this.updateRevertValue();

if (previousFormValue) {
this.reset(previousFormValue);
}
});
}

ngOnDestroy(): void {
this._finalValueSubject.next(this._form.value);
this._finalValueSubject.complete();
}

/** Whether the attached form is in a valid state. */
isValid(): boolean|null {
return this._form.valid;
}

/** Set the form's current value as what it will be set to on revert/reset. */
updateRevertValue(): void {
this._revertFormValue = this._form.value;
}

/** Tells the table to close the edit popup. */
close(): void {
this._editEventDispatcher.editing.next(null);
}

/**
* Closes the edit if the enter key is not down.
* Otherwise, sets _closePending to true so that the edit will close on the
* next enter keyup.
*/
closeAfterEnterKeypress(): void {
// If the enter key is currently pressed, delay closing the popup so that
// the keyUp event does not cause it to immediately reopen.
if (this._enterPressed) {
this._closePending = true;
} else {
this.close();
}
}

/**
* Called on Enter keyup/keydown.
* Closes the edit if pending. Otherwise just updates _enterPressed.
*/
trackEnterPressForClose(pressed: boolean): void {
if (this._closePending) {
this.close();
return;
}

this._enterPressed = pressed;
}

/**
* Resets the form value to the specified value or the previously set
* revert value.
*/
reset(value?: FormValue): void {
this._form.reset(value || this._revertFormValue);
}
}
9 changes: 9 additions & 0 deletions src/cdk-experimental/popover-edit/index.ts
@@ -0,0 +1,9 @@
/**
* @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
*/

export * from './public-api';

0 comments on commit 16ce33a

Please sign in to comment.