Skip to content

Commit a12027c

Browse files
committed
feat(popover-edit): experimental popover edit for tables (mvp)
1 parent bd66e5c commit a12027c

33 files changed

+1892
-6
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
/src/cdk-experimental/** @jelbourn
9090
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
9191
/src/cdk-experimental/scrolling/** @mmalerba
92+
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
9293

9394
# Docs examples & guides
9495
/guides/** @jelbourn
@@ -130,6 +131,7 @@
130131
/src/dev-app/paginator/** @andrewseguin
131132
/src/dev-app/platform/** @jelbourn @devversion
132133
/src/dev-app/portal/** @jelbourn
134+
/src/dev-app/popover-edit/** @kseamon @andrewseguin
133135
/src/dev-app/progress-bar/** @jelbourn @crisbeto @josephperrott
134136
/src/dev-app/progress-spinner/** @jelbourn @crisbeto @josephperrott
135137
/src/dev-app/radio/** @jelbourn @devversion

packages.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ CDK_TARGETS = ["//src/cdk"] + ["//src/cdk/%s" % p for p in CDK_PACKAGES]
2424

2525
CDK_EXPERIMENTAL_PACKAGES = [
2626
"dialog",
27+
"popover-edit",
2728
"scrolling",
2829
]
2930

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package(default_visibility=["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
4+
5+
ng_module(
6+
name = "popover-edit",
7+
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
8+
module_name = "@angular/cdk-experimental/popover-edit",
9+
deps = [
10+
"@npm//@angular/common",
11+
"@npm//@angular/core",
12+
"@npm//@angular/forms",
13+
"@npm//rxjs",
14+
"//src/cdk/a11y",
15+
"//src/cdk/overlay",
16+
"//src/cdk/portal",
17+
],
18+
)
19+
20+
ng_test_library(
21+
name = "popover_edit_test_sources",
22+
srcs = glob(["**/*.spec.ts"]),
23+
deps = [
24+
":popover-edit",
25+
"@npm//@angular/common",
26+
"@npm//@angular/forms",
27+
"@npm//rxjs",
28+
"//src/cdk/collections",
29+
"//src/cdk/table",
30+
],
31+
)
32+
33+
ng_web_test_suite(
34+
name = "unit_tests",
35+
deps = [":popover_edit_test_sources"]
36+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/** Selector for finding table cells. */
10+
export const CELL_SELECTOR = '.cdk-cell, .mat-cell, td';
11+
12+
/** Selector for finding table rows. */
13+
export const ROW_SELECTOR = '.cdk-row, .mat-row, tr';
14+
15+
/** CSS class added to the edit lens pane. */
16+
export const EDIT_PANE_CLASS = 'cdk-edit-pane';
17+
18+
/** Selector for finding the edit lens pane. */
19+
export const EDIT_PANE_SELECTOR = '.' + EDIT_PANE_CLASS;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Injectable} from '@angular/core';
10+
import {Observable, Subject, timer} from 'rxjs';
11+
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
12+
13+
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
14+
import {closest} from './polyfill';
15+
16+
/** The delay between mouse out events and hiding hover content. */
17+
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
18+
19+
/**
20+
* Service for sharing delegated events and state for triggering table edits.
21+
*/
22+
@Injectable()
23+
export class EditEventDispatcher {
24+
/** A subject that indicates which table cell is currently editing. */
25+
readonly editing = new Subject<Element|null>();
26+
27+
/** A subject that indicates which table row is currently hovered. */
28+
readonly hovering = new Subject<Element|null>();
29+
30+
/** A subject that emits mouse move events for table rows. */
31+
readonly mouseMove = new Subject<Element|null>();
32+
33+
/** The table cell that has an active edit lens (or null). */
34+
private _currentlyEditing: Element|null = null;
35+
36+
private readonly _hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
37+
private readonly _editingDistinct = this.editing.pipe(distinctUntilChanged(), share());
38+
39+
constructor() {
40+
this._editingDistinct.subscribe(cell => {
41+
this._currentlyEditing = cell;
42+
});
43+
}
44+
45+
/**
46+
* Gets an Observable that emits true when the specified element's cell
47+
* is editing and false when not.
48+
*/
49+
editingCell(element: Element|EventTarget): Observable<boolean> {
50+
let cell: Element|null = null;
51+
52+
return this._editingDistinct.pipe(
53+
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
54+
distinctUntilChanged(),
55+
);
56+
}
57+
58+
/**
59+
* Stops editing for the specified cell. If the specified cell is not the current
60+
* edit cell, does nothing.
61+
*/
62+
doneEditingCell(element: Element|EventTarget): void {
63+
const cell = closest(element, CELL_SELECTOR);
64+
65+
if (this._currentlyEditing === cell) {
66+
this.editing.next(null);
67+
}
68+
}
69+
70+
/**
71+
* Gets an Observable that emits true when the specified element's row
72+
* is being hovered over and false when not. Hovering is defined as when
73+
* the mouse has momentarily stopped moving over the cell.
74+
*/
75+
hoveringOnRow(element: Element|EventTarget): Observable<boolean> {
76+
let row: Element|null = null;
77+
78+
return this._hoveringDistinct.pipe(
79+
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
80+
audit(
81+
(hovering) => hovering ? this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
82+
timer(DEFAULT_MOUSE_OUT_DELAY_MS)),
83+
distinctUntilChanged(),
84+
);
85+
}
86+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Injectable, OnDestroy, Self} from '@angular/core';
10+
import {ControlContainer} from '@angular/forms';
11+
import {Subject} from 'rxjs';
12+
import {take} from 'rxjs/operators';
13+
14+
import {EditEventDispatcher} from './edit-event-dispatcher';
15+
16+
/**
17+
* Service used for communication between the form within the edit lens and the
18+
* table that launched it. Provided by CdkPopoverEditControl within the lens.
19+
*/
20+
@Injectable()
21+
export class EditRef<FormValue> implements OnDestroy {
22+
/** Emits the final value of this edit instance before closing. */
23+
private readonly _finalValueSubject = new Subject<FormValue>();
24+
readonly finalValue = this._finalValueSubject.asObservable();
25+
26+
/** The value to set the form back to on revert. */
27+
private _revertFormValue: FormValue;
28+
29+
constructor(
30+
@Self() private readonly _form: ControlContainer,
31+
private readonly _editEventDispatcher: EditEventDispatcher) {}
32+
33+
/**
34+
* Called by the host directive's OnInit hook. Reads the initial state of the
35+
* form and overrides it with persisted state from previous openings, if
36+
* applicable.
37+
*/
38+
init(previousFormValue: FormValue|undefined): void {
39+
// Wait for either the first value to be set, then override it with
40+
// the previously entered value, if any.
41+
this._form.valueChanges!.pipe(take(1)).subscribe(() => {
42+
this.updateRevertValue();
43+
44+
if (previousFormValue) {
45+
this.reset(previousFormValue);
46+
}
47+
});
48+
}
49+
50+
ngOnDestroy() {
51+
this._finalValueSubject.next(this._form.value);
52+
this._finalValueSubject.complete();
53+
}
54+
55+
/** Whether the attached form is in a valid state. */
56+
isValid(): boolean|null {
57+
return this._form.valid;
58+
}
59+
60+
/** Set the form's current value as what it will be set to on revert/reset. */
61+
updateRevertValue(): void {
62+
this._revertFormValue = this._form.value;
63+
}
64+
65+
/** Tells the table to close the edit popup. */
66+
close(): void {
67+
this._editEventDispatcher.editing.next(null);
68+
}
69+
70+
/**
71+
* Resets the form value to the specified value or the previously set
72+
* revert value.
73+
*/
74+
reset(value?: FormValue): void {
75+
this._form.reset(value || this._revertFormValue);
76+
}
77+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';

0 commit comments

Comments
 (0)