Skip to content

Commit fbe596a

Browse files
committed
feat(popover-edit): allow tabbing from popup to next/previous cell
1 parent f6edba3 commit fbe596a

10 files changed

+446
-61
lines changed

src/cdk-experimental/popover-edit/edit-event-dispatcher.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
1212

1313
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
1414
import {closest} from './polyfill';
15+
import {EditRef} from './edit-ref';
1516

1617
/** The delay between mouse out events and hiding hover content. */
1718
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
@@ -30,6 +31,12 @@ export class EditEventDispatcher {
3031
/** A subject that emits mouse move events for table rows. */
3132
readonly mouseMove = new Subject<Element|null>();
3233

34+
/** The EditRef for the currently active edit lens (if any). */
35+
get editRef(): EditRef<any>|null {
36+
return this._editRef;
37+
}
38+
private _editRef: EditRef<any>|null = null;
39+
3340
/** The table cell that has an active edit lens (or null). */
3441
private _currentlyEditing: Element|null = null;
3542

@@ -67,6 +74,20 @@ export class EditEventDispatcher {
6774
}
6875
}
6976

77+
/** Sets the currently active EditRef. */
78+
setActiveEditRef(ref: EditRef<any>) {
79+
this._editRef = ref;
80+
}
81+
82+
/** Unsets the currently active EditRef, if the specified editRef is active. */
83+
unsetActiveEditRef(ref: EditRef<any>) {
84+
if (this._editRef !== ref) {
85+
return;
86+
}
87+
88+
this._editRef = null;
89+
}
90+
7091
/**
7192
* Gets an Observable that emits true when the specified element's row
7293
* is being hovered over and false when not. Hovering is defined as when

src/cdk-experimental/popover-edit/edit-ref.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class EditRef<FormValue> implements OnDestroy {
2323
private readonly _finalValueSubject = new Subject<FormValue>();
2424
readonly finalValue: Observable<FormValue> = this._finalValueSubject.asObservable();
2525

26+
/** Emits when the user tabs out of this edit lens before closing. */
27+
private readonly _blurredSubject = new Subject<void>();
28+
readonly blurred: Observable<void> = this._blurredSubject.asObservable();
29+
2630
/** The value to set the form back to on revert. */
2731
private _revertFormValue: FormValue;
2832

@@ -37,7 +41,9 @@ export class EditRef<FormValue> implements OnDestroy {
3741

3842
constructor(
3943
@Self() private readonly _form: ControlContainer,
40-
private readonly _editEventDispatcher: EditEventDispatcher) {}
44+
private readonly _editEventDispatcher: EditEventDispatcher) {
45+
this._editEventDispatcher.setActiveEditRef(this);
46+
}
4147

4248
/**
4349
* Called by the host directive's OnInit hook. Reads the initial state of the
@@ -57,6 +63,7 @@ export class EditRef<FormValue> implements OnDestroy {
5763
}
5864

5965
ngOnDestroy(): void {
66+
this._editEventDispatcher.unsetActiveEditRef(this);
6067
this._finalValueSubject.next(this._form.value);
6168
this._finalValueSubject.complete();
6269
}
@@ -76,6 +83,11 @@ export class EditRef<FormValue> implements OnDestroy {
7683
this._editEventDispatcher.editing.next(null);
7784
}
7885

86+
/** Notifies the active edit that the user has moved focus out of the lens. */
87+
blur(): void {
88+
this._blurredSubject.next();
89+
}
90+
7991
/**
8092
* Closes the edit if the enter key is not down.
8193
* Otherwise, sets _closePending to true so that the edit will close on the
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 {Inject, Injectable, NgZone} from '@angular/core';
10+
import {DOCUMENT} from '@angular/common';
11+
import {FocusTrap, InteractivityChecker} from '@angular/cdk/a11y';
12+
import {Observable, Subject} from 'rxjs';
13+
14+
/** Value indicating whether focus left the target area before or after the enclosed elements. */
15+
export const enum FocusEscapeNotifierDirection {
16+
START,
17+
END,
18+
}
19+
20+
/**
21+
* Like FocusTrap, but rather than trapping focus within a dom region, notifies subscribers when
22+
* focus leaves the region.
23+
*/
24+
export class FocusEscapeNotifier extends FocusTrap {
25+
private _escapeSubject = new Subject<FocusEscapeNotifierDirection>();
26+
27+
constructor(
28+
element: HTMLElement,
29+
checker: InteractivityChecker,
30+
ngZone: NgZone,
31+
document: Document) {
32+
super(element, checker, ngZone, document, true /* deferAnchors */);
33+
34+
// The focus trap adds "anchors" at the beginning and end of a trapped region that redirect
35+
// focus. We override that redirect behavior here with simply emitting on a stream.
36+
this.startAnchorListener = () => {
37+
this._escapeSubject.next(FocusEscapeNotifierDirection.START);
38+
return true;
39+
};
40+
this.endAnchorListener = () => {
41+
this._escapeSubject.next(FocusEscapeNotifierDirection.END);
42+
return true;
43+
};
44+
45+
this.attachAnchors();
46+
}
47+
48+
escapes(): Observable<FocusEscapeNotifierDirection> {
49+
return this._escapeSubject.asObservable();
50+
}
51+
}
52+
53+
/** Factory that allows easy instantiation of focus escape notifiers. */
54+
@Injectable({providedIn: 'root'})
55+
export class FocusEscapeNotifierFactory {
56+
private _document: Document;
57+
58+
constructor(
59+
private _checker: InteractivityChecker,
60+
private _ngZone: NgZone,
61+
@Inject(DOCUMENT) _document: any) {
62+
63+
this._document = _document;
64+
}
65+
66+
/**
67+
* Creates a focus escape notifier region around the given element.
68+
* @param element The element around which focus will be monitored.
69+
* @returns The created focus escape notifier instance.
70+
*/
71+
create(element: HTMLElement): FocusEscapeNotifier {
72+
return new FocusEscapeNotifier(element, this._checker, this._ngZone, this._document);
73+
}
74+
}

src/cdk-experimental/popover-edit/lens-directives.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
6161
ngOnInit(): void {
6262
this.editRef.init(this.preservedFormValue);
6363
this.editRef.finalValue.subscribe(this.preservedFormValueChange);
64+
this.editRef.blurred.subscribe(() => this._handleBlur());
6465
}
6566

6667
ngOnDestroy(): void {
@@ -97,7 +98,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
9798
switch (this.clickOutBehavior) {
9899
case 'submit':
99100
// Manually cause the form to submit before closing.
100-
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
101+
this._triggerFormSubmit();
101102
// Fall through
102103
case 'close':
103104
this.editRef.close();
@@ -106,6 +107,18 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
106107
break;
107108
}
108109
}
110+
111+
/** Triggers submit on tab out if clickOutBehavior is 'submit'. */
112+
private _handleBlur(): void {
113+
if (this.clickOutBehavior === 'submit') {
114+
// Manually cause the form to submit before closing.
115+
this._triggerFormSubmit();
116+
}
117+
}
118+
119+
private _triggerFormSubmit() {
120+
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
121+
}
109122
}
110123

111124
/** Reverts the form to its initial or previously submitted state on click. */

src/cdk-experimental/popover-edit/popover-edit-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
1010
import {OverlayModule} from '@angular/cdk/overlay';
1111
import {
1212
CdkPopoverEdit,
13+
CdkPopoverEditTabOut,
1314
CdkRowHoverContent,
1415
CdkEditable,
1516
CdkEditOpen,
@@ -26,6 +27,7 @@ import {
2627

2728
const EXPORTED_DECLARATIONS = [
2829
CdkPopoverEdit,
30+
CdkPopoverEditTabOut,
2931
CdkRowHoverContent,
3032
CdkEditControl,
3133
CdkEditRevert,

0 commit comments

Comments
 (0)