Skip to content

Commit 615070a

Browse files
crisbetojelbourn
authored andcommitted
fix(popover-edit): unable to close by pressing on enter on some screen readers (#16466)
* Fixes not being able to close the overlay by pressing Enter on the close button when using some screen readers (tested on NVDA, but I expect it to work similarly in others). The issue comes from the fact that we keep track of whether the user is pressing Enter through `keyup` and `keydown` events, however screen readers emit fake `click` events on buttons instead of `keyup` and `keydown`. * Fixes not being able to close the overlay by clicking on the close button with a mouse, if the keyboard was used to reach the button (e.g. by tabbing to it). This issue also comes from the fact that we were keeping track of keyboard events and assuming that the user will continue using a keyboard to close the overlay. * Gets rid of a bunch of keyboard event tracking logic. **Disclaimer:** I had to introduce a `setTimeout` when trapping focus inside the overlay, in order to prevent the form inside the overlay from being submitted immediately when opening through a `keydown` on Enter. From what I can tell this shouldn't really have an effect on existing tests since we have other timers that are running at the same time.
1 parent 67532db commit 615070a

File tree

5 files changed

+20
-60
lines changed

5 files changed

+20
-60
lines changed

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

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,6 @@ export class EditRef<FormValue> implements OnDestroy {
3030
/** The value to set the form back to on revert. */
3131
private _revertFormValue: FormValue;
3232

33-
/**
34-
* The flags are used to track whether a keyboard enter press is in progress at the same time
35-
* as other events that would cause the edit lens to close. We must track this so that the
36-
* Enter keyup event does not fire after we close as it would cause the edit to immediately
37-
* reopen.
38-
*/
39-
private _enterPressed = false;
40-
private _closePending = false;
41-
4233
constructor(
4334
@Self() private readonly _form: ControlContainer,
4435
private readonly _editEventDispatcher: EditEventDispatcher,
@@ -88,34 +79,6 @@ export class EditRef<FormValue> implements OnDestroy {
8879
this._blurredSubject.next();
8980
}
9081

91-
/**
92-
* Closes the edit if the enter key is not down.
93-
* Otherwise, sets _closePending to true so that the edit will close on the
94-
* next enter keyup.
95-
*/
96-
closeAfterEnterKeypress(): void {
97-
// If the enter key is currently pressed, delay closing the popup so that
98-
// the keyUp event does not cause it to immediately reopen.
99-
if (this._enterPressed) {
100-
this._closePending = true;
101-
} else {
102-
this.close();
103-
}
104-
}
105-
106-
/**
107-
* Called on Enter keyup/keydown.
108-
* Closes the edit if pending. Otherwise just updates _enterPressed.
109-
*/
110-
trackEnterPressForClose(pressed: boolean): void {
111-
if (this._closePending) {
112-
this.close();
113-
return;
114-
}
115-
116-
this._enterPressed = pressed;
117-
}
118-
11982
/**
12083
* Resets the form value to the specified value or the previously set
12184
* revert value.

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
8989
if (this.ignoreSubmitUnlessValid && !this.editRef.isValid()) { return; }
9090

9191
this.editRef.updateRevertValue();
92-
this.editRef.closeAfterEnterKeypress();
92+
this.editRef.close();
9393
}
9494

9595
/** Called on Escape keyup. Closes the edit. */
@@ -128,21 +128,9 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
128128
// to avoid double event listeners, we need to use `HostListener`. Once Ivy is the default, we
129129
// can move this back into `host`.
130130
// tslint:disable:no-host-decorator-in-concrete
131-
@HostListener('keydown')
132-
_handleKeydown() {
133-
this.editRef.trackEnterPressForClose(true);
134-
}
135-
136-
// In Ivy the `host` metadata will be merged, whereas in ViewEngine it is overridden. In order
137-
// to avoid double event listeners, we need to use `HostListener`. Once Ivy is the default, we
138-
// can move this back into `host`.
139-
// tslint:disable:no-host-decorator-in-concrete
140-
@HostListener('keyup', ['$event'])
141-
_handleKeyup(event: KeyboardEvent) {
142-
// TODO(crisbeto): should use cdk/keycodes once the tests are reworked to use cdk/testing.
143-
if (event.key === 'Enter') {
144-
this.editRef.trackEnterPressForClose(false);
145-
} else if (event.key === 'Escape') {
131+
@HostListener('keydown', ['$event'])
132+
_handleKeydown(event: KeyboardEvent) {
133+
if (event.key === 'Escape') {
146134
this.close();
147135
}
148136
}
@@ -204,6 +192,8 @@ export class CdkEditClose<FormValue> {
204192
// tslint:disable:no-host-decorator-in-concrete
205193
@HostListener('click')
206194
closeEdit(): void {
207-
this.editRef.closeAfterEnterKeypress();
195+
// Note that we use `click` here, rather than a keyboard event, because some screen readers
196+
// will emit a fake click event instead of an enter keyboard event on buttons.
197+
this.editRef.close();
208198
}
209199
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ abstract class BaseTestComponent {
120120
openLens(rowIndex = 0, cellIndex = 1) {
121121
this.focusEditCell(rowIndex, cellIndex);
122122
this.getEditCell(rowIndex, cellIndex)
123-
.dispatchEvent(new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'}));
123+
.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key: 'Enter'}));
124124
flush();
125125
}
126126

@@ -772,7 +772,7 @@ cdkPopoverEditTabOut`, fakeAsync(() => {
772772
component.openLens();
773773

774774
component.getNameInput()!.dispatchEvent(
775-
new KeyboardEvent('keyup', {bubbles: true, key: 'Escape'}));
775+
new KeyboardEvent('keydown', {bubbles: true, key: 'Escape'}));
776776

777777
expect(component.lensIsOpen()).toBe(false);
778778
clearLeftoverTimers();

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
133133
share(),
134134
).subscribe(this.editEventDispatcher.allRows);
135135

136-
fromEvent<KeyboardEvent>(element, 'keyup').pipe(
136+
fromEvent<KeyboardEvent>(element, 'keydown').pipe(
137137
filter(event => event.key === 'Enter'),
138138
toClosest(CELL_SELECTOR),
139139
takeUntil(this.destroyed),
@@ -276,7 +276,14 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
276276
this.template!,
277277
this.viewContainerRef,
278278
{$implicit: this.context}));
279-
this.focusTrap!.focusInitialElement();
279+
280+
// We have to defer trapping focus, because doing so too early can cause the form inside
281+
// the overlay to be submitted immediately if it was opened on an Enter keydown event.
282+
this.services.ngZone.runOutsideAngular(() => {
283+
setTimeout(() => {
284+
this.focusTrap!.focusInitialElement();
285+
});
286+
});
280287

281288
// Update the size of the popup initially and on subsequent changes to
282289
// scroll position and viewport size.

src/material-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ abstract class BaseTestComponent {
117117
openLens(rowIndex = 0, cellIndex = 1) {
118118
this.focusEditCell(rowIndex, cellIndex);
119119
this.getEditCell(rowIndex, cellIndex)
120-
.dispatchEvent(new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'}));
120+
.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key: 'Enter'}));
121121
flush();
122122
}
123123

@@ -691,7 +691,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
691691
component.openLens();
692692

693693
component.getInput()!.dispatchEvent(
694-
new KeyboardEvent('keyup', {bubbles: true, key: 'Escape'}));
694+
new KeyboardEvent('keydown', {bubbles: true, key: 'Escape'}));
695695

696696
expect(component.lensIsOpen()).toBe(false);
697697
clearLeftoverTimers();

0 commit comments

Comments
 (0)