Skip to content

Commit b567792

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

33 files changed

+1871
-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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 {Subject, timer} from 'rxjs';
10+
import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
11+
import {Injectable} from '@angular/core';
12+
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
13+
import {closest} from './polyfill';
14+
15+
/** The delay between mouse out events and hiding hover content. */
16+
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
17+
18+
/**
19+
* Service for sharing delegated events and state for triggering edits.
20+
*/
21+
@Injectable()
22+
export class EditEvents {
23+
/** A subject that indicates which table cell is currently editing. */
24+
readonly editing = new Subject<Element|null>();
25+
26+
/** A subject that indicates which table row is currently hovered. */
27+
readonly hovering = new Subject<Element|null>();
28+
29+
/** A subject that emits mouse move events for table rows. */
30+
readonly mouseMove = new Subject<Element|null>();
31+
32+
/** The table cell that has an active edit lens (or null). */
33+
private _currentlyEditing: Element|null = null;
34+
35+
private readonly _hoveringDistinct = this.hovering.pipe(distinctUntilChanged(), share());
36+
private readonly _editingDistinct = this.editing.pipe(distinctUntilChanged(), share());
37+
38+
constructor() {
39+
this._editingDistinct.subscribe(cell => {
40+
this._currentlyEditing = cell;
41+
});
42+
}
43+
44+
/**
45+
* Returns an Observable that emits true when the specified element's cell
46+
* is editing and false when not.
47+
*/
48+
editingCell(element: Element|EventTarget) {
49+
let cell: Element|null = null;
50+
51+
return this._editingDistinct.pipe(
52+
map(editCell => editCell === (cell || (cell = closest(element, CELL_SELECTOR)))),
53+
distinctUntilChanged(),
54+
);
55+
}
56+
57+
/**
58+
* Stops editing for the specified cell. If the specified cell is not the current
59+
* edit cell, does nothing.
60+
*/
61+
doneEditingCell(element: Element|EventTarget) {
62+
const cell = closest(element, CELL_SELECTOR);
63+
64+
if (this._currentlyEditing === cell) {
65+
this.editing.next(null);
66+
}
67+
}
68+
69+
/**
70+
* Returns an Observable that emits true when the specified element's row
71+
* is being hovered over and false when not. Hovering is defined as when
72+
* the mouse has momentarily stopped moving over the cell.
73+
*/
74+
hoveringOnRow(element: Element|EventTarget) {
75+
let row: Element|null = null;
76+
77+
return this._hoveringDistinct.pipe(
78+
map(hoveredRow => hoveredRow === (row || (row = closest(element, ROW_SELECTOR)))),
79+
audit((hovering) => hovering ?
80+
this.mouseMove.pipe(filter(hoveredRow => hoveredRow === row)) :
81+
timer(DEFAULT_MOUSE_OUT_DELAY_MS)),
82+
distinctUntilChanged(),
83+
);
84+
}
85+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 {Subject} from 'rxjs';
10+
import {first} from 'rxjs/operators';
11+
import {Injectable, OnDestroy, Self} from '@angular/core';
12+
import {ControlContainer} from '@angular/forms';
13+
import {EditEvents} from './edit-events';
14+
15+
/**
16+
* Service used for communication between the form within the edit lens and the
17+
* table that launched it. Provided by CdkPopoverEditControl within the lens.
18+
*/
19+
@Injectable()
20+
export class EditRef implements OnDestroy {
21+
/** Emits the final value of this edit instance before closing. */
22+
private readonly _finalValueSubject = new Subject<any>();
23+
readonly finalValue = this._finalValueSubject.asObservable();
24+
25+
/** The value to set the form back to on revert. */
26+
private _revertFormValue: any;
27+
28+
constructor(
29+
@Self() private readonly _form: ControlContainer,
30+
private readonly _editEvents: EditEvents, ) {}
31+
32+
/**
33+
* Called by the host directive's OnInit hook. Reads the initial state of the
34+
* form and overrides it with persisted state from previous openings, if
35+
* applicable.
36+
*/
37+
init(previousFormValue: any) {
38+
// Wait for either the first value to be set, then override it with
39+
// the previously entered value, if any.
40+
this._form.valueChanges!.pipe(first()).subscribe(() => {
41+
this.updateRevertValue();
42+
43+
if (previousFormValue) {
44+
this.reset(previousFormValue);
45+
}
46+
});
47+
}
48+
49+
ngOnDestroy() {
50+
this._finalValueSubject.next(this._form.value);
51+
this._finalValueSubject.complete();
52+
}
53+
54+
/** Whether the attached form is in a valid state. */
55+
isValid() {
56+
return this._form.valid;
57+
}
58+
59+
/** Set the form's current value as what it will be set to on revert/reset. */
60+
updateRevertValue() {
61+
this._revertFormValue = this._form.value;
62+
}
63+
64+
/** Tells the table to close the edit popup. */
65+
close() {
66+
this._editEvents.editing.next(null);
67+
}
68+
69+
/**
70+
* Resets the form value to the specified value or the previously set
71+
* revert value.
72+
*/
73+
reset(value?: any) {
74+
this._form.reset(value || this._revertFormValue);
75+
}
76+
}
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)