Replace KeyboardShortcutMixin by addShortcut() util in 5 components

Google-Bug-Id: b/199305453
Change-Id: I09997238cdfc8fca3e5391935519b2451dfe8cd7
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 50bb665..ae3eee5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -45,7 +45,6 @@
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {fireEvent} from '../../../utils/event-util';
 import {
-  KeyboardShortcutMixin,
   Shortcut,
   ShortcutSection,
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -65,11 +64,8 @@
   };
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-file-list-header')
-export class GrFileListHeader extends base {
+export class GrFileListHeader extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 74c079b..4fd5ff3 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -49,8 +49,10 @@
   SpecialFilePath,
 } from '../../../constants/constants';
 import {
+  addGlobalShortcut,
   descendedFromClass,
   isShiftPressed,
+  Key,
   modifierPressed,
   toggleClass,
 } from '../../../utils/dom-util';
@@ -319,11 +321,8 @@
 
   disconnected$ = new Subject();
 
-  get keyBindings() {
-    return {
-      esc: '_handleEscKey',
-    };
-  }
+  /** Called in disconnectedCallback. */
+  private cleanups: (() => void)[] = [];
 
   override keyboardShortcuts() {
     return {
@@ -415,6 +414,9 @@
           this.reporting.error(new Error('dynamic header/content mismatch'));
         }
       });
+    this.cleanups.push(
+      addGlobalShortcut({key: Key.ESC}, e => this._handleEscKey(e))
+    );
   }
 
   override disconnectedCallback() {
@@ -423,6 +425,8 @@
     this.fileCursor.unsetCursor();
     this._cancelDiffs();
     this.loadingTask?.cancel();
+    for (const cleanup of this.cleanups) cleanup();
+    this.cleanups = [];
     super.disconnectedCallback();
   }
 
@@ -1542,10 +1546,8 @@
     return undefined;
   }
 
-  _handleEscKey(e: IronKeyboardEvent) {
-    if (this.shortcuts.shouldSuppress(e) || this.shortcuts.modifierPressed(e)) {
-      return;
-    }
+  _handleEscKey(e: KeyboardEvent) {
+    if (this.shortcuts.shouldSuppress(e)) return;
     e.preventDefault();
     this._displayLine = false;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index cd79bba..e16c073 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -22,7 +22,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-messages-list_html';
 import {
-  KeyboardShortcutMixin,
   Shortcut,
   ShortcutSection,
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -201,11 +200,8 @@
   };
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-messages-list')
-export class GrMessagesList extends base {
+export class GrMessagesList extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index 541d877..c91ae5a 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -21,7 +21,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html';
 import {
-  KeyboardShortcutMixin,
   ShortcutSection,
   ShortcutListener,
   SectionView,
@@ -40,11 +39,8 @@
   shortcuts?: SectionView;
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-keyboard-shortcuts-dialog')
-export class GrKeyboardShortcutsDialog extends base {
+export class GrKeyboardShortcutsDialog extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 084f9f6..236f00f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -107,7 +107,7 @@
 import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
 import {GerritView} from '../../../services/router/router-model';
 import {assertIsDefined} from '../../../utils/common-util';
-import {toggleClass} from '../../../utils/dom-util';
+import {addGlobalShortcut, Key, toggleClass} from '../../../utils/dom-util';
 import {CursorMoveResult} from '../../../api/core';
 import {throttleWrap} from '../../../utils/async-util';
 import {changeComments$} from '../../../services/comments/comments-model';
@@ -281,11 +281,8 @@
     patchNum?: PatchSetNum;
   } = {};
 
-  get keyBindings() {
-    return {
-      esc: '_handleEscKey',
-    };
-  }
+  /** Called in disconnectedCallback. */
+  private cleanups: (() => void)[] = [];
 
   override keyboardShortcuts() {
     return {
@@ -373,6 +370,9 @@
       this.cursor.reInitCursor();
     };
     this.$.diffHost.addEventListener('render', this._onRenderHandler);
+    this.cleanups.push(
+      addGlobalShortcut({key: Key.ESC}, e => this._handleEscKey(e))
+    );
   }
 
   override disconnectedCallback() {
@@ -381,6 +381,8 @@
     if (this._onRenderHandler) {
       this.$.diffHost.removeEventListener('render', this._onRenderHandler);
     }
+    for (const cleanup of this.cleanups) cleanup();
+    this.cleanups = [];
     super.disconnectedCallback();
   }
 
@@ -531,10 +533,8 @@
     this._setReviewed(!this.$.reviewed.checked);
   }
 
-  _handleEscKey(e: IronKeyboardEvent) {
+  _handleEscKey(e: KeyboardEvent) {
     if (this.shortcuts.shouldSuppress(e)) return;
-    if (this.shortcuts.modifierPressed(e)) return;
-
     e.preventDefault();
     this.$.diffHost.displayLine = false;
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index cc35c3c..d45ca0e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -512,7 +512,7 @@
       assert(computeContainerClassStub.lastCall.calledWithExactly(
           false, 'SIDE_BY_SIDE', true));
 
-      MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+      MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
       assert(computeContainerClassStub.lastCall.calledWithExactly(
           false, 'SIDE_BY_SIDE', false));
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index a629d0e..e7137e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -20,12 +20,12 @@
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-autocomplete-dropdown_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {IronFitMixin} from '../../../mixins/iron-fit-mixin/iron-fit-mixin';
 import {customElement, property, observe} from '@polymer/decorators';
 import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior';
 import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
 import {fireEvent} from '../../../utils/event-util';
+import {addShortcut, Key} from '../../../utils/dom-util';
 
 export interface GrAutocompleteDropdown {
   $: {
@@ -53,10 +53,7 @@
 }
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = IronFitMixin(
-  KeyboardShortcutMixin(PolymerElement),
-  IronFitBehavior as IronFitBehavior
-);
+const base = IronFitMixin(PolymerElement, IronFitBehavior as IronFitBehavior);
 
 @customElement('gr-autocomplete-dropdown')
 export class GrAutocompleteDropdown extends base {
@@ -91,15 +88,8 @@
   @property({type: Array})
   suggestions: Item[] = [];
 
-  get keyBindings() {
-    return {
-      up: '_handleUp',
-      down: '_handleDown',
-      enter: '_handleEnter',
-      esc: '_handleEscape',
-      tab: '_handleTab',
-    };
-  }
+  /** Called in disconnectedCallback. */
+  private cleanups: (() => void)[] = [];
 
   // visible for testing
   cursor = new GrCursorManager();
@@ -110,8 +100,29 @@
     this.cursor.focusOnMove = true;
   }
 
+  override connectedCallback() {
+    super.connectedCallback();
+    this.cleanups.push(
+      addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.ESC}, _ => this._handleEscape())
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+    );
+  }
+
   override disconnectedCallback() {
     this.cursor.unsetCursor();
+    for (const cleanup of this.cleanups) cleanup();
+    this.cleanups = [];
     super.disconnectedCallback();
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index bb47dbc0..86de3b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -50,7 +50,7 @@
 
   test('escape key', () => {
     const closeSpy = sinon.spy(element, 'close');
-    MockInteractions.pressAndReleaseKeyOn(element, 27);
+    MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
     flush();
     assert.isTrue(closeSpy.called);
   });
@@ -59,7 +59,7 @@
     const handleTabSpy = sinon.spy(element, '_handleTab');
     const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
-    MockInteractions.pressAndReleaseKeyOn(element, 9);
+    MockInteractions.pressAndReleaseKeyOn(element, 9, null, 'Tab');
     assert.isTrue(handleTabSpy.called);
     assert.equal(element.cursor.index, 0);
     assert.isTrue(itemSelectedStub.called);
@@ -73,7 +73,7 @@
     const handleEnterSpy = sinon.spy(element, '_handleEnter');
     const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
-    MockInteractions.pressAndReleaseKeyOn(element, 13);
+    MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'Enter');
     assert.isTrue(handleEnterSpy.called);
     assert.equal(element.cursor.index, 0);
     assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
@@ -85,11 +85,11 @@
   test('down key', () => {
     element.isHidden = true;
     const nextSpy = sinon.spy(element.cursor, 'next');
-    MockInteractions.pressAndReleaseKeyOn(element, 40);
+    MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
     assert.isFalse(nextSpy.called);
     assert.equal(element.cursor.index, 0);
     element.isHidden = false;
-    MockInteractions.pressAndReleaseKeyOn(element, 40);
+    MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
     assert.isTrue(nextSpy.called);
     assert.equal(element.cursor.index, 1);
   });
@@ -97,13 +97,13 @@
   test('up key', () => {
     element.isHidden = true;
     const prevSpy = sinon.spy(element.cursor, 'previous');
-    MockInteractions.pressAndReleaseKeyOn(element, 38);
+    MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
     assert.isFalse(prevSpy.called);
     assert.equal(element.cursor.index, 0);
     element.isHidden = false;
     element.cursor.setCursorAtIndex(1);
     assert.equal(element.cursor.index, 1);
-    MockInteractions.pressAndReleaseKeyOn(element, 38);
+    MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
     assert.isTrue(prevSpy.called);
     assert.equal(element.cursor.index, 0);
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 524b197..8e84aa2 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -22,7 +22,6 @@
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-autocomplete_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {property, customElement, observe} from '@polymer/decorators';
 import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import {PaperInputElementExt} from '../../../types/types';
@@ -65,11 +64,8 @@
 export type AutocompleteCommitEvent =
   CustomEvent<AutocompleteCommitEventDetail>;
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-autocomplete')
-export class GrAutocomplete extends base {
+export class GrAutocomplete extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index deb45d3..6b2e5c4 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -84,8 +84,6 @@
 
 @customElement('gr-comment-thread')
 export class GrCommentThread extends PolymerElement {
-  // KeyboardShortcutMixin Not used in this element rather other elements tests
-
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index f4179f4..2b56de6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -24,10 +24,10 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-dropdown_html';
 import {getBaseUrl} from '../../../utils/url-util';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
 import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
 import {property, customElement, observe} from '@polymer/decorators';
+import {addShortcut, Key} from '../../../utils/dom-util';
 
 const REL_NOOPENER = 'noopener';
 const REL_EXTERNAL = 'external';
@@ -67,11 +67,8 @@
   bold?: boolean;
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-dropdown')
-export class GrDropdown extends base {
+export class GrDropdown extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -121,14 +118,8 @@
   @property({type: Array})
   disabledIds: string[] = [];
 
-  get keyBindings() {
-    return {
-      down: '_handleDown',
-      'enter space': '_handleEnter',
-      tab: '_handleTab',
-      up: '_handleUp',
-    };
-  }
+  /** Called in disconnectedCallback. */
+  private cleanups: (() => void)[] = [];
 
   // Used within the tests so needs to be non-private.
   cursor = new GrCursorManager();
@@ -139,15 +130,36 @@
     this.cursor.focusOnMove = true;
   }
 
+  override connectedCallback() {
+    super.connectedCallback();
+    this.cleanups.push(
+      addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.SPACE}, e => this._handleEnter(e))
+    );
+  }
+
   override disconnectedCallback() {
     this.cursor.unsetCursor();
+    for (const cleanup of this.cleanups) cleanup();
+    this.cleanups = [];
     super.disconnectedCallback();
   }
 
   /**
    * Handle the up key.
    */
-  _handleUp(e: MouseEvent) {
+  _handleUp(e: Event) {
     if (this.$.dropdown.opened) {
       e.preventDefault();
       e.stopPropagation();
@@ -160,7 +172,7 @@
   /**
    * Handle the down key.
    */
-  _handleDown(e: MouseEvent) {
+  _handleDown(e: Event) {
     if (this.$.dropdown.opened) {
       e.preventDefault();
       e.stopPropagation();
@@ -173,7 +185,7 @@
   /**
    * Handle the tab key.
    */
-  _handleTab(e: MouseEvent) {
+  _handleTab(e: Event) {
     if (this.$.dropdown.opened) {
       // Tab in a native select is a no-op. Emulate this.
       e.preventDefault();
@@ -184,7 +196,7 @@
   /**
    * Handle the enter key.
    */
-  _handleEnter(e: MouseEvent) {
+  _handleEnter(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     if (this.$.dropdown.opened) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
index e14d523..393f44e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
@@ -170,18 +170,18 @@
     test('down', () => {
       const stub = sinon.stub(element.cursor, 'next');
       assert.isFalse(element.$.dropdown.opened);
-      MockInteractions.pressAndReleaseKeyOn(element, 40);
+      MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
       assert.isTrue(element.$.dropdown.opened);
-      MockInteractions.pressAndReleaseKeyOn(element, 40);
+      MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
       assert.isTrue(stub.called);
     });
 
     test('up', () => {
       const stub = sinon.stub(element.cursor, 'previous');
       assert.isFalse(element.$.dropdown.opened);
-      MockInteractions.pressAndReleaseKeyOn(element, 38);
+      MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
       assert.isTrue(element.$.dropdown.opened);
-      MockInteractions.pressAndReleaseKeyOn(element, 38);
+      MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
       assert.isTrue(stub.called);
     });
 
@@ -189,7 +189,7 @@
       // Because enter and space are handled by the same fn, we need only to
       // test one.
       assert.isFalse(element.$.dropdown.opened);
-      MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+      MockInteractions.pressAndReleaseKeyOn(element, 32, null, ' ');
       assert.isTrue(element.$.dropdown.opened);
 
       const el = queryAndAssert<HTMLAnchorElement>(
@@ -197,7 +197,7 @@
         ':not([hidden]) a'
       );
       const stub = sinon.stub(el, 'click');
-      MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+      MockInteractions.pressAndReleaseKeyOn(element, 32, null, ' ');
       assert.isTrue(stub.called);
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 434da1f..337d595 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -23,7 +23,6 @@
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-textarea_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {appContext} from '../../../services/app-context';
 import {customElement, property} from '@polymer/decorators';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
@@ -33,7 +32,7 @@
   Item,
   ItemSelectedEvent,
 } from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {IronKeyboardEvent} from '../../../types/events';
+import {addShortcut, Key} from '../../../utils/dom-util';
 
 const MAX_ITEMS_DROPDOWN = 10;
 
@@ -85,11 +84,8 @@
   }
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
 @customElement('gr-textarea')
-export class GrTextarea extends base {
+export class GrTextarea extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -150,21 +146,39 @@
 
   disableEnterKeyForSelectingEmoji = false;
 
-  get keyBindings() {
-    return {
-      esc: '_handleEscKey',
-      tab: '_handleTabKey',
-      enter: '_handleEnterByKey',
-      up: '_handleUpKey',
-      down: '_handleDownKey',
-    };
-  }
+  /** Called in disconnectedCallback. */
+  private cleanups: (() => void)[] = [];
 
   constructor() {
     super();
     this.reporting = appContext.reportingService;
   }
 
+  override disconnectedCallback() {
+    super.disconnectedCallback();
+    for (const cleanup of this.cleanups) cleanup();
+    this.cleanups = [];
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.cleanups.push(
+      addShortcut(this, {key: Key.UP}, e => this._handleUpKey(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.DOWN}, e => this._handleDownKey(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.TAB}, e => this._handleTabKey(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.ENTER}, e => this._handleEnterByKey(e))
+    );
+    this.cleanups.push(
+      addShortcut(this, {key: Key.ESC}, e => this._handleEscKey(e))
+    );
+  }
+
   override ready() {
     super.ready();
     if (this.monospace) {
@@ -238,16 +252,11 @@
     this._setEmoji(this.$.emojiSuggestions.getCurrentText());
   }
 
-  _handleEnterByKey(e: IronKeyboardEvent) {
+  _handleEnterByKey(e: KeyboardEvent) {
     // Enter should have newline behavior if the picker is closed or if the user
     // has only typed ':'. Also make sure that shortcuts aren't clobbered.
     if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
-      if (
-        !e.detail.keyboardEvent?.metaKey &&
-        !e.detail.keyboardEvent?.ctrlKey
-      ) {
-        this.indent(e);
-      }
+      this.indent(e);
       return;
     }
 
@@ -420,7 +429,7 @@
     );
   }
 
-  private indent(e: IronKeyboardEvent): void {
+  private indent(e: KeyboardEvent): void {
     if (!document.queryCommandSupported('insertText')) {
       return;
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 7e59692..318c720 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -20,7 +20,6 @@
 import {GrTextarea} from './gr-textarea';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {IronKeyboardEvent} from '../../../types/events';
 import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 
 const basicFixture = fixtureFromElement('gr-textarea');
@@ -238,34 +237,12 @@
     const indentCommand = sinon.stub(document, 'execCommand');
     element.$.textarea.value = '    a';
     element._handleEnterByKey(
-      new CustomEvent('keydown', {
-        detail: {keyboardEvent: {keyCode: 13}},
-      }) as IronKeyboardEvent
+      new KeyboardEvent('keydown', {key: 'Enter', keyCode: 13})
     );
     await flush();
     assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n    ']);
   });
 
-  test('ctrl+enter and meta+enter do not indent', async () => {
-    const indentCommand = sinon.stub(document, 'execCommand');
-    element.$.textarea.value = '    a';
-    element._handleEnterByKey(
-      new CustomEvent('keydown', {
-        detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
-      }) as IronKeyboardEvent
-    );
-    await flush();
-    assert.isTrue(indentCommand.notCalled);
-
-    element._handleEnterByKey(
-      new CustomEvent('keydown', {
-        detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
-      }) as IronKeyboardEvent
-    );
-    await flush();
-    assert.isTrue(indentCommand.notCalled);
-  });
-
   test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
     const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
     element.$.emojiSuggestions.dispatchEvent(
@@ -301,38 +278,78 @@
 
     test('escape key', () => {
       const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        27,
+        null,
+        'Escape'
+      );
       assert.isFalse(resetSpy.called);
       setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        27,
+        null,
+        'Escape'
+      );
       assert.isTrue(resetSpy.called);
       assert.isFalse(!element.$.emojiSuggestions.isHidden);
     });
 
     test('up key', () => {
       const upSpy = sinon.spy(element.$.emojiSuggestions, 'cursorUp');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        38,
+        null,
+        'ArrowUp'
+      );
       assert.isFalse(upSpy.called);
       setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        38,
+        null,
+        'ArrowUp'
+      );
       assert.isTrue(upSpy.called);
     });
 
     test('down key', () => {
       const downSpy = sinon.spy(element.$.emojiSuggestions, 'cursorDown');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        40,
+        null,
+        'ArrowDown'
+      );
       assert.isFalse(downSpy.called);
       setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        40,
+        null,
+        'ArrowDown'
+      );
       assert.isTrue(downSpy.called);
     });
 
     test('enter key', () => {
       const enterSpy = sinon.spy(element.$.emojiSuggestions, 'getCursorTarget');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        13,
+        null,
+        'Enter'
+      );
       assert.isFalse(enterSpy.called);
       setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.textarea,
+        13,
+        null,
+        'Enter'
+      );
       assert.isTrue(enterSpy.called);
       flush();
       assert.equal(element.text, '💯');