Skip to content

feat(popover-edit): allow tabbing from popup to next/previous cell #15793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/cdk-experimental/popover-edit/edit-event-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';

import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
import {closest} from './polyfill';
import {EditRef} from './edit-ref';

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

/** The EditRef for the currently active edit lens (if any). */
get editRef(): EditRef<any>|null {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be in a follow-up change, but could we avoid using any here in favor of a generic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be tricky - the type is generic in the lens (refers to the shape of the form data) but different edits in the same table could have different lenses and this must refer to all of them.

It’d be fine to use unknown here but I don’t know what TS versions we have to support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make it unknown in a follow-up. As of Angular v8 we support 3.4 as the minimum

return this._editRef;
}
private _editRef: EditRef<any>|null = null;

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

Expand Down Expand Up @@ -67,6 +74,20 @@ export class EditEventDispatcher {
}
}

/** Sets the currently active EditRef. */
setActiveEditRef(ref: EditRef<any>) {
this._editRef = ref;
}

/** Unsets the currently active EditRef, if the specified editRef is active. */
unsetActiveEditRef(ref: EditRef<any>) {
if (this._editRef !== ref) {
return;
}

this._editRef = 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
Expand Down
14 changes: 13 additions & 1 deletion src/cdk-experimental/popover-edit/edit-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class EditRef<FormValue> implements OnDestroy {
private readonly _finalValueSubject = new Subject<FormValue>();
readonly finalValue: Observable<FormValue> = this._finalValueSubject.asObservable();

/** Emits when the user tabs out of this edit lens before closing. */
private readonly _blurredSubject = new Subject<void>();
readonly blurred: Observable<void> = this._blurredSubject.asObservable();

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

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

constructor(
@Self() private readonly _form: ControlContainer,
private readonly _editEventDispatcher: EditEventDispatcher) {}
private readonly _editEventDispatcher: EditEventDispatcher) {
this._editEventDispatcher.setActiveEditRef(this);
}

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

ngOnDestroy(): void {
this._editEventDispatcher.unsetActiveEditRef(this);
this._finalValueSubject.next(this._form.value);
this._finalValueSubject.complete();
}
Expand All @@ -76,6 +83,11 @@ export class EditRef<FormValue> implements OnDestroy {
this._editEventDispatcher.editing.next(null);
}

/** Notifies the active edit that the user has moved focus out of the lens. */
blur(): void {
this._blurredSubject.next();
}

/**
* Closes the edit if the enter key is not down.
* Otherwise, sets _closePending to true so that the edit will close on the
Expand Down
74 changes: 74 additions & 0 deletions src/cdk-experimental/popover-edit/focus-escape-notifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @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 {Inject, Injectable, NgZone} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {FocusTrap, InteractivityChecker} from '@angular/cdk/a11y';
import {Observable, Subject} from 'rxjs';

/** Value indicating whether focus left the target area before or after the enclosed elements. */
export const enum FocusEscapeNotifierDirection {
START,
END,
}

/**
* Like FocusTrap, but rather than trapping focus within a dom region, notifies subscribers when
* focus leaves the region.
*/
export class FocusEscapeNotifier extends FocusTrap {
private _escapeSubject = new Subject<FocusEscapeNotifierDirection>();

constructor(
element: HTMLElement,
checker: InteractivityChecker,
ngZone: NgZone,
document: Document) {
super(element, checker, ngZone, document, true /* deferAnchors */);

// The focus trap adds "anchors" at the beginning and end of a trapped region that redirect
// focus. We override that redirect behavior here with simply emitting on a stream.
this.startAnchorListener = () => {
this._escapeSubject.next(FocusEscapeNotifierDirection.START);
return true;
};
this.endAnchorListener = () => {
this._escapeSubject.next(FocusEscapeNotifierDirection.END);
return true;
};

this.attachAnchors();
}

escapes(): Observable<FocusEscapeNotifierDirection> {
return this._escapeSubject.asObservable();
}
}

/** Factory that allows easy instantiation of focus escape notifiers. */
@Injectable({providedIn: 'root'})
export class FocusEscapeNotifierFactory {
private _document: Document;

constructor(
private _checker: InteractivityChecker,
private _ngZone: NgZone,
@Inject(DOCUMENT) _document: any) {

this._document = _document;
}

/**
* Creates a focus escape notifier region around the given element.
* @param element The element around which focus will be monitored.
* @returns The created focus escape notifier instance.
*/
create(element: HTMLElement): FocusEscapeNotifier {
return new FocusEscapeNotifier(element, this._checker, this._ngZone, this._document);
}
}
15 changes: 14 additions & 1 deletion src/cdk-experimental/popover-edit/lens-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
ngOnInit(): void {
this.editRef.init(this.preservedFormValue);
this.editRef.finalValue.subscribe(this.preservedFormValueChange);
this.editRef.blurred.subscribe(() => this._handleBlur());
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -97,7 +98,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
switch (this.clickOutBehavior) {
case 'submit':
// Manually cause the form to submit before closing.
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
this._triggerFormSubmit();
// Fall through
case 'close':
this.editRef.close();
Expand All @@ -106,6 +107,18 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
break;
}
}

/** Triggers submit on tab out if clickOutBehavior is 'submit'. */
private _handleBlur(): void {
if (this.clickOutBehavior === 'submit') {
// Manually cause the form to submit before closing.
this._triggerFormSubmit();
}
}

private _triggerFormSubmit() {
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than "submitting" the form via the DOM, couldn't we call the submit handler ourselves?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could alternatively call .submit() on the form, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do need the form itself to submit in case the user added any submit handlers.

}
}

/** Reverts the form to its initial or previously submitted state on click. */
Expand Down
2 changes: 2 additions & 0 deletions src/cdk-experimental/popover-edit/popover-edit-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
import {
CdkPopoverEdit,
CdkPopoverEditTabOut,
CdkRowHoverContent,
CdkEditable,
CdkEditOpen,
Expand All @@ -26,6 +27,7 @@ import {

const EXPORTED_DECLARATIONS = [
CdkPopoverEdit,
CdkPopoverEditTabOut,
CdkRowHoverContent,
CdkEditControl,
CdkEditRevert,
Expand Down
Loading