Skip to content

Commit 813f514

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

33 files changed

+1851
-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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
private readonly _finalValueSubject = new Subject<any>();
22+
readonly finalValue = this._finalValueSubject.asObservable();
23+
24+
private _revertFormValue: any;
25+
26+
constructor(
27+
@Self() private readonly _form: ControlContainer,
28+
private readonly _editEvents: EditEvents, ) {}
29+
30+
/**
31+
* Called by the host directive's OnInit hook. Reads the initial state of the
32+
* form and overrides it with persisted state from previous openings, if
33+
* applicable.
34+
*/
35+
init(previousFormValue: any) {
36+
// Wait for either the first value to be set, then override it with
37+
// the previously entered value, if any.
38+
this._form.valueChanges!.pipe(first()).subscribe(() => {
39+
this.updateRevertValue();
40+
41+
if (previousFormValue) {
42+
this.reset(previousFormValue);
43+
}
44+
});
45+
}
46+
47+
ngOnDestroy() {
48+
this._finalValueSubject.next(this._form.value);
49+
this._finalValueSubject.complete();
50+
}
51+
52+
/** Whether the attached form is in a valid state. */
53+
isValid() {
54+
return this._form.valid;
55+
}
56+
57+
/** Set the form's current value as what it will be set to on revert/reset. */
58+
updateRevertValue() {
59+
this._revertFormValue = this._form.value;
60+
}
61+
62+
/** Tells the table to close the edit popup. */
63+
close() {
64+
this._editEvents.editing.next(null);
65+
}
66+
67+
/**
68+
* Resets the form value to the specified value or the previously set
69+
* revert value.
70+
*/
71+
reset(value?: any) {
72+
this._form.reset(value || this._revertFormValue);
73+
}
74+
}
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)