Merge changes I756c55f8,Ic9f0dd6d,I05a330de into stable-3.5

* changes:
  Add REST endpoint for commits included in refs
  Use JGit's RevWalk.getMergedInto instead of IncludedInResolver
  Update JGit to 60b81c5a9280e44fa48d533a61f915382b2b9ce2
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 cc76145..f0d8935 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
@@ -421,7 +421,9 @@
       });
     this.cleanups.push(
       addGlobalShortcut({key: Key.ESC}, _ => this._handleEscKey()),
-      addShortcut(this, {key: Key.ENTER}, _ => this.handleOpenFile())
+      addShortcut(this, {key: Key.ENTER}, _ => this.handleOpenFile(), {
+        shouldSuppress: true,
+      })
     );
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index bce4024..7493e2f 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -691,18 +691,13 @@
       css`
         .title {
           color: var(--deemphasized-text-color);
-          padding-left: var(--metadata-horizontal-padding);
-        }
-        h4 {
           display: flex;
           align-self: flex-end;
+          margin-left: 20px;
         }
         gr-button {
           display: flex;
         }
-        h4 {
-          margin-left: 20px;
-        }
         gr-button iron-icon {
           color: inherit;
           --iron-icon-height: 18px;
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
similarity index 72%
rename from polygerrit-ui/app/elements/custom-dark-theme_test.js
rename to polygerrit-ui/app/elements/custom-dark-theme_test.ts
index 768a461..71e0740 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
@@ -14,17 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {removeTheme} from '../styles/themes/dark-theme.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {GrApp} from './gr-app';
 
 const basicFixture = fixtureFromElement('gr-app');
 
 suite('gr-app custom dark theme tests', () => {
-  let element;
+  let element: GrApp;
   setup(async () => {
     window.localStorage.setItem('dark-theme', 'true');
 
@@ -36,7 +35,6 @@
 
   teardown(() => {
     window.localStorage.removeItem('dark-theme');
-    removeTheme();
     // The app sends requests to server. This can lead to
     // unexpected gr-alert elements in document.body
     document.body.querySelectorAll('gr-alert').forEach(grAlert => {
@@ -45,19 +43,17 @@
   });
 
   test('should tried to load dark theme', () => {
-    assert.isTrue(
-        !!document.head.querySelector('#dark-theme')
-    );
+    assert.isTrue(!!document.head.querySelector('#dark-theme'));
   });
 
   test('applies the right theme', () => {
     assert.equal(
-        getComputedStyleValue('--header-background-color', element)
-            .toLowerCase(),
-        '#3c4043');
+      getComputedStyleValue('--header-background-color', element).toLowerCase(),
+      '#3c4043'
+    );
     assert.equal(
-        getComputedStyleValue('--footer-background-color', element)
-            .toLowerCase(),
-        '#3c4043');
+      getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+      '#3c4043'
+    );
   });
 });
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.ts
similarity index 69%
rename from polygerrit-ui/app/elements/custom-light-theme_test.js
rename to polygerrit-ui/app/elements/custom-light-theme_test.ts
index c6e9642..80a7cab 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.ts
@@ -14,21 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {stubRestApi} from '../test/test-utils.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import '../styles/themes/app-theme';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {stubRestApi} from '../test/test-utils';
+import {GrApp} from './gr-app';
+import {
+  createAccountDetailWithId,
+  createServerInfo,
+} from '../test/test-data-generators';
 
 const basicFixture = fixtureFromElement('gr-app');
 
 suite('gr-app custom light theme tests', () => {
-  let element;
+  let element: GrApp;
   setup(async () => {
     window.localStorage.removeItem('dark-theme');
-    stubRestApi('getConfig').returns(Promise.resolve({test: 'config'}));
-    stubRestApi('getAccount').returns(Promise.resolve({}));
+    stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
+    stubRestApi('getAccount').returns(
+      Promise.resolve(createAccountDetailWithId())
+    );
     stubRestApi('getDiffComments').returns(Promise.resolve({}));
     stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
     stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -52,12 +59,12 @@
 
   test('applies the right theme', () => {
     assert.equal(
-        getComputedStyleValue('--header-background-color', element)
-            .toLowerCase(),
-        '#f1f3f4');
+      getComputedStyleValue('--header-background-color', element).toLowerCase(),
+      '#f1f3f4'
+    );
     assert.equal(
-        getComputedStyleValue('--footer-background-color', element)
-            .toLowerCase(),
-        'transparent');
+      getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+      'transparent'
+    );
   });
 });
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index b99a17b..fc22a58 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -580,7 +580,10 @@
   /**
    * Computes a number of unresolved comment threads in a given file and path.
    */
-  computeUnresolvedNum(file: PatchSetFile | PatchNumOnly) {
+  computeUnresolvedNum(
+    file: PatchSetFile | PatchNumOnly,
+    ignorePatchsetLevelComments?: boolean
+  ) {
     let comments: Comment[] = [];
     let drafts: Comment[] = [];
 
@@ -595,7 +598,11 @@
 
     comments = comments.concat(drafts);
     const threads = createCommentThreads(comments);
-    const unresolvedThreads = threads.filter(isUnresolved);
+    let unresolvedThreads = threads.filter(isUnresolved);
+    if (ignorePatchsetLevelComments)
+      unresolvedThreads = unresolvedThreads.filter(
+        thread => !isPatchsetLevel(thread)
+      );
     return unresolvedThreads.length;
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 7efd2f8..6003a2f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -950,7 +950,7 @@
       // Safari is not binding newly created comment-thread
       // with the slot somehow, replace itself will rebind it
       // @see Issue 11182
-      if (lastEl && lastEl.replaceWith) {
+      if (isSafari() && lastEl && lastEl.replaceWith) {
         lastEl.replaceWith(lastEl);
       }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 0d6cadc..857ffa2 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -381,7 +381,10 @@
     );
     const commentThreadString = pluralize(commentThreadCount, 'comment');
 
-    const unresolvedCount = changeComments.computeUnresolvedNum({patchNum});
+    const unresolvedCount = changeComments.computeUnresolvedNum(
+      {patchNum},
+      true
+    );
     const unresolvedString =
       unresolvedCount === 0 ? '' : `${unresolvedCount} unresolved`;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
index 0fe1fe2..28ebbac 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
@@ -351,15 +351,15 @@
       ],
       abc: [],
       // Patchset level comment does not contribute to the count
-      [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: {
+      [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
         id: '27dcee4d_f7b77cfa',
         message: 'test',
         patch_set: 1,
         unresolved: true,
         updated: '2017-10-11 20:48:40.000000000',
-      },
+      }],
     };
-    element.changeComments = new ChangeComments(comments, {}, {}, 123);
+    element.changeComments = new ChangeComments(comments);
 
     assert.equal(element._computePatchSetCommentsString(
         element.changeComments, 1), ' (3 comments, 1 unresolved)');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 256e956..127ac69 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -21,10 +21,6 @@
 import '../../../styles/gr-menu-page-styles';
 import '../../../styles/gr-page-nav-styles';
 import '../../../styles/shared-styles';
-import {
-  applyTheme as applyDarkTheme,
-  removeTheme as removeDarkTheme,
-} from '../../../styles/themes/dark-theme';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../gr-change-table-editor/gr-change-table-editor';
 import '../../shared/gr-button/gr-button';
@@ -74,6 +70,7 @@
   TimeFormat,
 } from '../../../constants/constants';
 import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
+import {windowLocationReload} from '../../../utils/dom-util';
 
 const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
   'changes_per_page',
@@ -537,13 +534,14 @@
   _handleToggleDark() {
     if (this._isDark) {
       window.localStorage.removeItem('dark-theme');
-      removeDarkTheme();
     } else {
       window.localStorage.setItem('dark-theme', 'true');
-      applyDarkTheme();
     }
-    this._isDark = !!window.localStorage.getItem('dark-theme');
-    fireAlert(this, `Theme changed to ${this._isDark ? 'dark' : 'light'}.`);
+    this.reloadPage();
+  }
+
+  reloadPage() {
+    windowLocationReload();
   }
 
   _showHttpAuth(config?: ServerInfo) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 78c4a62..81e4fc4 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -100,6 +100,7 @@
     </gr-page-nav>
     <div class="main gr-form-styles">
       <h1 class="heading-1">User Settings</h1>
+      <h2 id="Theme">Theme</h2>
       <section class="darkToggle">
         <div class="toggle">
           <paper-toggle-button
@@ -108,13 +109,10 @@
             on-change="_handleToggleDark"
             on-click="_onTapDarkToggle"
           ></paper-toggle-button>
-          <div id="darkThemeToggleLabel">Dark theme (alpha)</div>
+          <div id="darkThemeToggleLabel">
+            Dark theme (the toggle reloads the page)
+          </div>
         </div>
-        <p>
-          Gerrit's dark theme is in early alpha, and almost definitely will not
-          play nicely with themes set by specific Gerrit hosts. Filing feedback
-          via the link in the app footer is strongly encouraged!
-        </p>
       </section>
       <h2 id="Profile" class$="[[_computeHeaderClass(_accountInfoChanged)]]">
         Profile
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 1165f1e..61876fe 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -16,7 +16,6 @@
  */
 
 import '../../../test/common-test-setup-karma';
-import {getComputedStyleValue} from '../../../utils/dom-util';
 import './gr-settings-view';
 import {GrSettingsView} from './gr-settings-view';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
@@ -130,23 +129,24 @@
       await element._testOnly_loadingPromise;
   });
 
-  test('theme changing', () => {
+  test('theme changing', async () => {
+    const reloadStub = sinon.stub(element, 'reloadPage');
+
     window.localStorage.removeItem('dark-theme');
     assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
     const themeToggle = queryAndAssert(
       element,
       '.darkToggle paper-toggle-button'
     );
-    /* const themeToggle = element.shadowRoot
-        .querySelector('.darkToggle paper-toggle-button'); */
     MockInteractions.tap(themeToggle);
     assert.isTrue(window.localStorage.getItem('dark-theme') === 'true');
-    assert.equal(
-      getComputedStyleValue('--primary-text-color', document.body),
-      '#e8eaed'
-    );
+    assert.isTrue(reloadStub.calledOnce);
+
+    element._isDark = true;
+    await flush();
     MockInteractions.tap(themeToggle);
     assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
+    assert.isTrue(reloadStub.calledTwice);
   });
 
   test('calls the title-change event', () => {
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 19f12c5..a26fa08 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -28,6 +28,7 @@
   Key,
   Modifier,
   Binding,
+  shouldSuppress,
 } from '../../utils/dom-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
 
@@ -154,34 +155,8 @@
 
   shouldSuppress(e: KeyboardEvent) {
     if (this.shortcutsDisabled) return true;
+    if (shouldSuppress(e)) return true;
 
-    // Note that when you listen on document, then `e.currentTarget` will be the
-    // document and `e.target` will be `<gr-app>` due to shadow dom, but by
-    // using the composedPath() you can actually find the true origin of the
-    // event.
-    const rootTarget = e.composedPath()[0];
-    if (!isElementTarget(rootTarget)) return false;
-    const tagName = rootTarget.tagName;
-    const type = rootTarget.getAttribute('type');
-
-    if (
-      // Suppress shortcuts on <input> and <textarea>, but not on
-      // checkboxes, because we want to enable workflows like 'click
-      // mark-reviewed and then press ] to go to the next file'.
-      (tagName === 'INPUT' && type !== 'checkbox') ||
-      tagName === 'TEXTAREA' ||
-      // Suppress shortcuts if the key is 'enter'
-      // and target is an anchor or button or paper-tab.
-      (e.keyCode === 13 &&
-        (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
-    ) {
-      return true;
-    }
-    const path: EventTarget[] = e.composedPath() ?? [];
-    for (const el of path) {
-      if (!isElementTarget(el)) continue;
-      if (el.tagName === 'GR-OVERLAY') return true;
-    }
     // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
     let key = `${e.key}:${e.type}`;
     if (this.isInSpecificComboKeyMode(ComboKey.G)) key = 'g+' + key;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index a24a666..05c6b7d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -249,10 +249,3 @@
 export function applyTheme() {
   document.head.appendChild(getStyleEl());
 }
-
-export function removeTheme() {
-  const darkThemeEls = document.head.querySelectorAll('#dark-theme');
-  if (darkThemeEls.length) {
-    darkThemeEls.forEach(darkThemeEl => darkThemeEl.remove());
-  }
-}
diff --git a/polygerrit-ui/app/styles/themes/dark-theme_test.ts b/polygerrit-ui/app/styles/themes/dark-theme_test.ts
deleted file mode 100644
index 16e609e..0000000
--- a/polygerrit-ui/app/styles/themes/dark-theme_test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma';
-import {applyTheme, removeTheme} from './dark-theme';
-
-suite('dark-theme test', () => {
-  test('apply and remove theme', () => {
-    applyTheme();
-    assert.equal(document.head.querySelectorAll('#dark-theme').length, 1);
-    removeTheme();
-    assert.isEmpty(document.head.querySelectorAll('#dark-theme'));
-  });
-});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 949c268..bd5504a 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,6 +30,7 @@
   registerTestCleanup,
   addIronOverlayBackdropStyleEl,
   removeIronOverlayBackdropStyleEl,
+  removeThemeStyles,
 } from './test-utils';
 import {safeTypesBridge} from '../utils/safe-types-util';
 import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
@@ -197,6 +198,7 @@
   cleanupTestUtils();
   checkGlobalSpace();
   removeIronOverlayBackdropStyleEl();
+  removeThemeStyles();
   cancelAllTasks();
   cleanUpStorage();
   // Reset state
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 39c30ad..557fe71 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -155,6 +155,13 @@
   el.parentNode?.removeChild(el);
 }
 
+export function removeThemeStyles() {
+  // Do not remove the light theme, because it is only added once statically,
+  // not once per gr-app instantiation.
+  // document.head.querySelector('#light-theme')?.remove();
+  document.head.querySelector('#dark-theme')?.remove();
+}
+
 export function waitUntil(
   predicate: () => boolean,
   maxMillis = 100
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index b6dece0..e2fa8fe 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -398,10 +398,16 @@
 export function addShortcut(
   element: HTMLElement,
   shortcut: Binding,
-  listener: (e: KeyboardEvent) => void
+  listener: (e: KeyboardEvent) => void,
+  options: {
+    shouldSuppress: boolean;
+  } = {
+    shouldSuppress: false,
+  }
 ) {
   const wrappedListener = (e: KeyboardEvent) => {
     if (e.repeat) return;
+    if (options.shouldSuppress && shouldSuppress(e)) return;
     if (eventMatchesShortcut(e, shortcut)) {
       listener(e);
     }
@@ -417,3 +423,43 @@
 export function shiftPressed(e: KeyboardEvent) {
   return e.shiftKey;
 }
+
+/**
+ * When you listen on keyboard events, then within Gerrit's web app you may want
+ * to avoid firing in certain common scenarios such as key strokes from <input>
+ * elements. But this can also be undesirable, for example Ctrl-Enter from
+ * <input> should trigger a save event.
+ *
+ * The shortcuts-service has a stateful method `shouldSuppress()` with
+ * reporting functionality, which delegates to here.
+ */
+export function shouldSuppress(e: KeyboardEvent): boolean {
+  // Note that when you listen on document, then `e.currentTarget` will be the
+  // document and `e.target` will be `<gr-app>` due to shadow dom, but by
+  // using the composedPath() you can actually find the true origin of the
+  // event.
+  const rootTarget = e.composedPath()[0];
+  if (!isElementTarget(rootTarget)) return false;
+  const tagName = rootTarget.tagName;
+  const type = rootTarget.getAttribute('type');
+
+  if (
+    // Suppress shortcuts on <input> and <textarea>, but not on
+    // checkboxes, because we want to enable workflows like 'click
+    // mark-reviewed and then press ] to go to the next file'.
+    (tagName === 'INPUT' && type !== 'checkbox') ||
+    tagName === 'TEXTAREA' ||
+    // Suppress shortcuts if the key is 'enter'
+    // and target is an anchor or button or paper-tab.
+    (e.keyCode === 13 &&
+      (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
+  ) {
+    return true;
+  }
+  const path: EventTarget[] = e.composedPath() ?? [];
+  for (const el of path) {
+    if (!isElementTarget(el)) continue;
+    if (el.tagName === 'GR-OVERLAY') return true;
+  }
+  return false;
+}
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 2993c0e..9dd5be2 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -22,6 +22,7 @@
   getEventPath,
   Modifier,
   querySelectorAll,
+  shouldSuppress,
   strToClassName,
 } from './dom-util';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -29,6 +30,22 @@
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import {queryAndAssert} from '../test/test-utils';
 
+async function keyEventOn(
+  el: HTMLElement,
+  callback: (e: KeyboardEvent) => void,
+  keyCode = 75,
+  key = 'k'
+): Promise<KeyboardEvent> {
+  let resolve: (e: KeyboardEvent) => void;
+  const promise = new Promise<KeyboardEvent>(r => (resolve = r));
+  el.addEventListener('keydown', (e: KeyboardEvent) => {
+    callback(e);
+    resolve(e);
+  });
+  MockInteractions.keyDownOn(el, keyCode, null, key);
+  return await promise;
+}
+
 class TestEle extends PolymerElement {
   static get is() {
     return 'dom-util-test-element';
@@ -266,4 +283,53 @@
       assert.isTrue(eventMatchesShortcut(e, sShift));
     });
   });
+
+  suite('shouldSuppress', () => {
+    test('do not suppress shortcut event from <div>', async () => {
+      await keyEventOn(document.createElement('div'), e => {
+        assert.isFalse(shouldSuppress(e));
+      });
+    });
+
+    test('suppress shortcut event from <input>', async () => {
+      await keyEventOn(document.createElement('input'), e => {
+        assert.isTrue(shouldSuppress(e));
+      });
+    });
+
+    test('suppress shortcut event from <textarea>', async () => {
+      await keyEventOn(document.createElement('textarea'), e => {
+        assert.isTrue(shouldSuppress(e));
+      });
+    });
+
+    test('do not suppress shortcut event from checkbox <input>', async () => {
+      const inputEl = document.createElement('input');
+      inputEl.setAttribute('type', 'checkbox');
+      await keyEventOn(inputEl, e => {
+        assert.isFalse(shouldSuppress(e));
+      });
+    });
+
+    test('suppress shortcut event from children of <gr-overlay>', async () => {
+      const overlay = document.createElement('gr-overlay');
+      const div = document.createElement('div');
+      overlay.appendChild(div);
+      await keyEventOn(div, e => {
+        assert.isTrue(shouldSuppress(e));
+      });
+    });
+
+    test('suppress "enter" shortcut event from <a>', async () => {
+      await keyEventOn(document.createElement('a'), e => {
+        assert.isFalse(shouldSuppress(e));
+      });
+      await keyEventOn(
+        document.createElement('a'),
+        e => assert.isTrue(shouldSuppress(e)),
+        13,
+        'enter'
+      );
+    });
+  });
 });