Skip to content

Commit be3dc6f

Browse files
asyncLizcopybara-github
authored andcommitted
fix(dialog): immediate escape key not firing cancel event in Chrome 120
Fixes #5313 PiperOrigin-RevId: 592651305
1 parent 8912019 commit be3dc6f

File tree

1 file changed

+42
-0
lines changed

1 file changed

+42
-0
lines changed

dialog/internal/dialog.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ export class Dialog extends LitElement {
114114
@state() private hasActions = false;
115115
@state() private hasIcon = false;
116116

117+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1512224
118+
// Chrome v120 has a bug where escape keys do not trigger cancels. If we get
119+
// a dialog "close" event that is triggered without a "cancel" after an escape
120+
// keydown, then we need to manually trigger our closing logic.
121+
//
122+
// This bug occurs when pressing escape to close a dialog without first
123+
// interacting with the dialog's content.
124+
//
125+
// Cleanup tracking:
126+
// https://github.com/material-components/material-web/issues/5330
127+
// This can be removed when full CloseWatcher support added and the above bug
128+
// in Chromium is fixed to fire 'cancel' with one escape press and close with
129+
// multiple.
130+
private escapePressedWithoutCancel = false;
131+
117132
constructor() {
118133
super();
119134
if (!isServer) {
@@ -243,6 +258,8 @@ export class Dialog extends LitElement {
243258
role=${this.type === 'alert' ? 'alertdialog' : nothing}
244259
@cancel=${this.handleCancel}
245260
@click=${this.handleDialogClick}
261+
@close=${this.handleClose}
262+
@keydown=${this.handleKeydown}
246263
.returnValue=${this.returnValue || nothing}>
247264
<div class="container" @click=${this.handleContentClick}>
248265
<div class="headline">
@@ -328,6 +345,7 @@ export class Dialog extends LitElement {
328345
return;
329346
}
330347

348+
this.escapePressedWithoutCancel = false;
331349
const preventDefault = !redispatchEvent(this, event);
332350
// We always prevent default on the original dialog event since we'll
333351
// animate closing it before it actually closes.
@@ -339,6 +357,30 @@ export class Dialog extends LitElement {
339357
this.close();
340358
}
341359

360+
private handleClose() {
361+
if (!this.escapePressedWithoutCancel) {
362+
return;
363+
}
364+
365+
this.escapePressedWithoutCancel = false;
366+
this.dialog?.dispatchEvent(new Event('cancel', {cancelable: true}));
367+
}
368+
369+
private handleKeydown(event: KeyboardEvent) {
370+
if (event.key !== 'Escape') {
371+
return;
372+
}
373+
374+
// An escape key was pressed. If a "close" event fires next without a
375+
// "cancel" event first, then we know we're in the Chrome v120 bug.
376+
this.escapePressedWithoutCancel = true;
377+
// Wait a full task for the cancel/close event listeners to fire, then
378+
// reset the flag.
379+
setTimeout(() => {
380+
this.escapePressedWithoutCancel = false;
381+
});
382+
}
383+
342384
private async animateDialog(animation: DialogAnimation) {
343385
const {dialog, scrim, container, headline, content, actions} = this;
344386
if (!dialog || !scrim || !container || !headline || !content || !actions) {

0 commit comments

Comments
 (0)