Merge changes I5333fbcd,I3ee10e8e

* changes:
  Add keyboard shortcut for selecting the bulk action checkbox
  Move ShortcutsService from AppContext to DI
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 1f46530..ffa0dca 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -292,7 +292,7 @@
         <input
           type="checkbox"
           .checked=${this.checked}
-          @click=${() => this.handleChangeSelectionClick()}
+          @click=${() => this.toggleCheckbox()}
         />
       </td>
     `;
@@ -607,15 +607,6 @@
     `;
   }
 
-  private handleChangeSelectionClick() {
-    assertIsDefined(this.change, 'change');
-    this.checked = !this.checked;
-    if (this.checked)
-      this.getBulkActionsModel().addSelectedChangeNum(this.change._number);
-    else
-      this.getBulkActionsModel().removeSelectedChangeNum(this.change._number);
-  }
-
   private changeStatuses() {
     if (!this.change) return [];
     return changeStatuses(this.change);
@@ -672,6 +663,15 @@
     return str;
   }
 
+  toggleCheckbox() {
+    assertIsDefined(this.change, 'change');
+    this.checked = !this.checked;
+    if (this.checked)
+      this.getBulkActionsModel().addSelectedChangeNum(this.change._number);
+    else
+      this.getBulkActionsModel().removeSelectedChangeNum(this.change._number);
+  }
+
   // private but used in test
   computeTruncatedRepoDisplay() {
     if (!this.change?.project) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 657a458..068487c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -27,6 +27,8 @@
   BulkActionsModel,
 } from '../../../models/bulk-actions/bulk-actions-model';
 import {subscribe} from '../../lit/subscription-controller';
+import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
+import {queryAll} from '../../../utils/common-util';
 
 const NUMBER_FIXED_COLUMNS = 3;
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
@@ -320,6 +322,12 @@
     return cols;
   }
 
+  toggleChange(index: number) {
+    const items = queryAll<GrChangeListItem>(this, 'gr-change-list-item');
+    if (index >= items.length) throw new Error('invalid item index');
+    items[index].toggleCheckbox();
+  }
+
   // private but used in test
   computeItemSelected(index: number) {
     return index === this.selectedIndex;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index cd66fd4..4061ab0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -23,7 +23,7 @@
 import {ColumnNames, ScrollMode} from '../../../constants/constants';
 import {getRequirements} from '../../../utils/label-util';
 import {addGlobalShortcut, Key} from '../../../utils/dom-util';
-import {unique} from '../../../utils/common-util';
+import {assertIsDefined, unique} from '../../../utils/common-util';
 import {changeListStyles} from '../../../styles/gr-change-list-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -163,6 +163,9 @@
     this.shortcuts.addAbstract(Shortcut.REFRESH_CHANGE_LIST, () =>
       this.refreshChangeList()
     );
+    this.shortcuts.addAbstract(Shortcut.TOGGLE_CHECKBOX, () =>
+      this.toggleCheckbox()
+    );
     addGlobalShortcut({key: Key.ENTER}, () => this.openChange());
   }
 
@@ -279,6 +282,25 @@
     }
   }
 
+  private toggleCheckbox() {
+    assertIsDefined(this.selectedIndex, 'selectedIndex');
+    let selectedIndex = this.selectedIndex;
+    assertIsDefined(this.sections, 'sections');
+    const changeSections = queryAll<GrChangeListSection>(
+      this,
+      'gr-change-list-section'
+    );
+    for (let i = 0; i < this.sections.length; i++) {
+      if (selectedIndex >= this.sections[i].results.length) {
+        selectedIndex -= this.sections[i].results.length;
+        continue;
+      }
+      changeSections[i].toggleChange(selectedIndex);
+      return;
+    }
+    throw new Error('invalid selected index');
+  }
+
   private computeVisibleChangeTableColumns() {
     if (!this.config) return;
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index 0a019b9..b49c916 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -16,7 +16,11 @@
   waitUntil,
 } from '../../../test/test-utils';
 import {Key} from '../../../utils/dom-util';
-import {ColumnNames, TimeFormat} from '../../../constants/constants';
+import {
+  ColumnNames,
+  createDefaultPreferences,
+  TimeFormat,
+} from '../../../constants/constants';
 import {AccountId, NumericChangeId} from '../../../types/common';
 import {
   createChange,
@@ -25,6 +29,14 @@
 } from '../../../test/test-data-generators';
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
+import {getAppContext} from '../../../services/app-context';
+import {fixture} from '@open-wc/testing-helpers';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {
+  ShortcutsService,
+  shortcutsServiceToken,
+} from '../../../services/shortcuts/shortcuts-service';
+import {html} from 'lit';
 
 const basicFixture = fixtureFromElement('gr-change-list');
 
@@ -197,22 +209,7 @@
     sinon.stub(element, 'computeLabelNames');
     element.sections = [{results: new Array(1)}, {results: new Array(2)}];
     element.selectedIndex = 0;
-    element.preferences = {
-      legacycid_in_change_table: true,
-      time_format: TimeFormat.HHMM_12,
-      change_table: [
-        'Subject',
-        'Status',
-        'Owner',
-        'Reviewers',
-        'Comments',
-        'Repo',
-        'Branch',
-        'Updated',
-        'Size',
-        ColumnNames.STATUS2,
-      ],
-    };
+    element.preferences = createDefaultPreferences();
     element.config = createServerInfo();
     element.changes = [
       {...createChange(), _number: 0 as NumericChangeId},
@@ -280,6 +277,90 @@
     assert.equal(element.selectedIndex, 0);
   });
 
+  test('toggle checkbox keyboard shortcut', async () => {
+    const flagsService = getAppContext().flagsService;
+    sinon.stub(flagsService, 'isEnabled').returns(true);
+    element = (
+      await fixture(
+        wrapInProvider(
+          html`<gr-change-list></gr-change-list>`,
+          shortcutsServiceToken,
+          new ShortcutsService(getAppContext().userModel, flagsService)
+        )
+      )
+    ).querySelector('gr-change-list')!;
+    await element.updateComplete;
+
+    const getCheckbox = (item: GrChangeListItem) =>
+      queryAndAssert<HTMLInputElement>(query(item, '.selection'), 'input');
+
+    sinon.stub(element, 'computeLabelNames');
+    element.sections = [{results: new Array(1)}, {results: new Array(2)}];
+    element.selectedIndex = 0;
+    element.preferences = createDefaultPreferences();
+    element.config = createServerInfo();
+    element.changes = [
+      {...createChange(), _number: 0 as NumericChangeId},
+      {...createChange(), _number: 1 as NumericChangeId},
+      {...createChange(), _number: 2 as NumericChangeId},
+    ];
+    // explicitly trigger sectionsChanged so that cursor stops are properly
+    // updated
+    await element.sectionsChanged();
+    await element.updateComplete;
+    const section = queryAndAssert<GrChangeListSection>(
+      element,
+      'gr-change-list-section'
+    );
+    await section.updateComplete;
+    const elementItems = queryAll<GrChangeListItem>(
+      section,
+      'gr-change-list-item'
+    );
+    assert.equal(elementItems.length, 3);
+
+    assert.isTrue(elementItems[0].hasAttribute('selected'));
+    await element.updateComplete;
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+
+    assert.isTrue(getCheckbox(elementItems[0]).checked);
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+    assert.isFalse(getCheckbox(elementItems[0]).checked);
+
+    pressKey(element, 'j');
+    await element.updateComplete;
+
+    assert.equal(element.selectedIndex, 1);
+    assert.isTrue(elementItems[1].hasAttribute('selected'));
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+
+    assert.isTrue(getCheckbox(elementItems[1]).checked);
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+    assert.isFalse(getCheckbox(elementItems[1]).checked);
+
+    pressKey(element, 'j');
+    await element.updateComplete;
+    assert.equal(element.selectedIndex, 2);
+    assert.isTrue(elementItems[2].hasAttribute('selected'));
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+
+    assert.isTrue(getCheckbox(elementItems[2]).checked);
+
+    pressKey(element, 'x');
+    await element.updateComplete;
+    assert.isFalse(getCheckbox(elementItems[2]).checked);
+  });
+
   test('no changes', async () => {
     element.changes = [];
     await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index b8cbd26..1aa5334 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -183,7 +183,10 @@
   getRemovedByReason,
   hasAttention,
 } from '../../../utils/attention-set-util';
-import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {
+  listen,
+  shortcutsServiceToken,
+} from '../../../services/shortcuts/shortcuts-service';
 import {LoadingStatus} from '../../../models/change/change-model';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {resolve, DIPolymerElement} from '../../../models/dependency';
@@ -600,7 +603,7 @@
   // Private but used in tests.
   readonly getCommentsModel = resolve(this, commentsModelToken);
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   private subscriptions: Subscription[] = [];
 
@@ -2654,7 +2657,7 @@
   }
 
   createTitle(shortcutName: Shortcut, section: ShortcutSection) {
-    return this.shortcuts.createTitle(shortcutName, section);
+    return this.getShortcutsService().createTitle(shortcutName, section);
   }
 
   _handleRevisionActionsChanged(
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 e5fe8b6..de6ea8e 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
@@ -32,11 +32,12 @@
   Shortcut,
   ShortcutSection,
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {getAppContext} from '../../../services/app-context';
 import {css, html, LitElement} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {when} from 'lit/directives/when';
 import {ifDefined} from 'lit/directives/if-defined';
+import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
+import {resolve} from '../../../models/dependency';
 
 @customElement('gr-file-list-header')
 export class GrFileListHeader extends LitElement {
@@ -110,7 +111,7 @@
   @query('#collapseBtn')
   collapseBtn?: GrButton;
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   // Caps the number of files that can be shown and have the 'show diffs' /
   // 'hide diffs' buttons still be functional.
@@ -427,7 +428,7 @@
   }
 
   private createTitle(shortcutName: Shortcut, section: ShortcutSection) {
-    return this.shortcuts.createTitle(shortcutName, section);
+    return this.getShortcutsService().createTitle(shortcutName, section);
   }
 }
 
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 a1d80aa..a0bb383 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
@@ -43,6 +43,7 @@
 import {paperStyles} from '../../../styles/gr-paper-styles';
 import {when} from 'lit/directives/when';
 import {ifDefined} from 'lit/directives/if-defined';
+import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
 
 /**
  * The content of the enum is also used in the UI for the button text.
@@ -319,7 +320,7 @@
 
   private readonly reporting = getAppContext().reportingService;
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   constructor() {
     super();
@@ -468,13 +469,13 @@
 
   private computeExpandAllTitle() {
     if (this.expandAllState === ExpandAllState.COLLAPSE_ALL) {
-      return this.shortcuts.createTitle(
+      return this.getShortcutsService().createTitle(
         Shortcut.COLLAPSE_ALL_MESSAGES,
         ShortcutSection.ACTIONS
       );
     }
     if (this.expandAllState === ExpandAllState.EXPAND_ALL) {
-      return this.shortcuts.createTitle(
+      return this.getShortcutsService().createTitle(
         Shortcut.EXPAND_ALL_MESSAGES,
         ShortcutSection.ACTIONS
       );
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 f53a342..73e5da3 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
@@ -13,8 +13,11 @@
   ShortcutSection,
   SectionView,
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {getAppContext} from '../../../services/app-context';
-import {ShortcutViewListener} from '../../../services/shortcuts/shortcuts-service';
+import {
+  shortcutsServiceToken,
+  ShortcutViewListener,
+} from '../../../services/shortcuts/shortcuts-service';
+import {resolve} from '../../../models/dependency';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -43,7 +46,7 @@
 
   private readonly shortcutListener: ShortcutViewListener;
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   constructor() {
     super();
@@ -151,11 +154,11 @@
 
   override connectedCallback() {
     super.connectedCallback();
-    this.shortcuts.addListener(this.shortcutListener);
+    this.getShortcutsService().addListener(this.shortcutListener);
   }
 
   override disconnectedCallback() {
-    this.shortcuts.removeListener(this.shortcutListener);
+    this.getShortcutsService().removeListener(this.shortcutListener);
     super.disconnectedCallback();
   }
 
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 1fa2413..4e99065 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
@@ -112,7 +112,10 @@
 import {isFalse, throttleWrap, until} from '../../../utils/async-util';
 import {filter, take, switchMap} from 'rxjs/operators';
 import {combineLatest, Subscription} from 'rxjs';
-import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {
+  listen,
+  shortcutsServiceToken,
+} from '../../../services/shortcuts/shortcuts-service';
 import {LoadingStatus} from '../../../models/change/change-model';
 import {DisplayLine} from '../../../api/diff';
 import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
@@ -371,7 +374,7 @@
   // Private but used in tests.
   readonly getCommentsModel = resolve(this, commentsModelToken);
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   _throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
 
@@ -1816,7 +1819,7 @@
   }
 
   createTitle(shortcutName: Shortcut, section: ShortcutSection) {
-    return this.shortcuts.createTitle(shortcutName, section);
+    return this.getShortcutsService().createTitle(shortcutName, section);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/lit/shortcut-controller.ts b/polygerrit-ui/app/elements/lit/shortcut-controller.ts
index 5755557..9d39cd7 100644
--- a/polygerrit-ui/app/elements/lit/shortcut-controller.ts
+++ b/polygerrit-ui/app/elements/lit/shortcut-controller.ts
@@ -5,9 +5,9 @@
  */
 import {ReactiveController, ReactiveControllerHost} from 'lit';
 import {Binding} from '../../utils/dom-util';
-import {ShortcutsService} from '../../services/shortcuts/shortcuts-service';
-import {getAppContext} from '../../services/app-context';
+import {shortcutsServiceToken} from '../../services/shortcuts/shortcuts-service';
 import {Shortcut} from '../../services/shortcuts/shortcuts-config';
+import {resolve} from '../../models/dependency';
 
 interface ShortcutListener {
   binding: Binding;
@@ -22,7 +22,10 @@
 type Cleanup = () => void;
 
 export class ShortcutController implements ReactiveController {
-  private readonly service: ShortcutsService = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(
+    this.host,
+    shortcutsServiceToken
+  );
 
   private readonly listenersLocal: ShortcutListener[] = [];
 
@@ -61,18 +64,24 @@
   }
 
   hostConnected() {
+    const shortcutsService = this.getShortcutsService();
     for (const {binding, listener} of this.listenersLocal) {
-      const cleanup = this.service.addShortcut(this.host, binding, listener, {
-        shouldSuppress: false,
-      });
+      const cleanup = shortcutsService.addShortcut(
+        this.host,
+        binding,
+        listener,
+        {
+          shouldSuppress: false,
+        }
+      );
       this.cleanups.push(cleanup);
     }
     for (const {shortcut, listener} of this.listenersAbstract) {
-      const cleanup = this.service.addShortcutListener(shortcut, listener);
+      const cleanup = shortcutsService.addShortcutListener(shortcut, listener);
       this.cleanups.push(cleanup);
     }
     for (const {binding, listener} of this.listenersGlobal) {
-      const cleanup = this.service.addShortcut(
+      const cleanup = shortcutsService.addShortcut(
         document.body,
         binding,
         listener
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index 817ec5c..dfebb8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -10,10 +10,11 @@
   Shortcut,
   ShortcutSection,
 } from '../../../services/shortcuts/shortcuts-config';
-import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, css, html} from 'lit';
 import {customElement, property} from 'lit/decorators';
+import {resolve} from '../../../models/dependency';
+import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -37,7 +38,7 @@
   @property({type: Object})
   change?: ChangeInfo;
 
-  private readonly shortcuts = getAppContext().shortcutsService;
+  private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
   static override get styles() {
     return [
@@ -73,7 +74,7 @@
     return html`
       <button
         role="checkbox"
-        title=${this.shortcuts.createTitle(
+        title=${this.getShortcutsService().createTitle(
           Shortcut.TOGGLE_CHANGE_STAR,
           ShortcutSection.ACTIONS
         )}
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 77150be..7596804 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -4,9 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {property} from '@polymer/decorators';
-import {PolymerElement} from '@polymer/polymer';
 import {check, Constructor} from '../../utils/common-util';
-import {getAppContext} from '../../services/app-context';
 import {
   Shortcut,
   ShortcutSection,
@@ -15,7 +13,9 @@
 import {
   SectionView,
   ShortcutListener,
+  shortcutsServiceToken,
 } from '../../services/shortcuts/shortcuts-service';
+import {DIPolymerElement, resolve} from '../../models/dependency';
 
 export {
   Shortcut,
@@ -25,7 +25,7 @@
   SectionView,
 };
 
-export const KeyboardShortcutMixin = <T extends Constructor<PolymerElement>>(
+export const KeyboardShortcutMixin = <T extends Constructor<DIPolymerElement>>(
   superClass: T
 ) => {
   /**
@@ -39,7 +39,7 @@
     // This enables `ShortcutSection` to be used in the html template.
     ShortcutSection = ShortcutSection;
 
-    private readonly shortcuts = getAppContext().shortcutsService;
+    private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
 
     /** Used to disable shortcuts when the element is not visible. */
     private observer?: IntersectionObserver;
@@ -103,7 +103,7 @@
       if (this.bindingsEnabled) return;
       this.bindingsEnabled = true;
 
-      this.shortcuts.attachHost(this, this.keyboardShortcuts());
+      this.getShortcutsService().attachHost(this, this.keyboardShortcuts());
     }
 
     /**
@@ -114,7 +114,7 @@
     private disableBindings() {
       if (!this.bindingsEnabled) return;
       this.bindingsEnabled = false;
-      this.shortcuts.detachHost(this);
+      this.getShortcutsService().detachHost(this);
     }
 
     private hasKeyboardShortcuts() {
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 9e3ca9b..6904529 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -21,7 +21,10 @@
   commentsModelToken,
 } from '../models/comments/comments-model';
 import {RouterModel} from './router/router-model';
-import {ShortcutsService} from './shortcuts/shortcuts-service';
+import {
+  ShortcutsService,
+  shortcutsServiceToken,
+} from './shortcuts/shortcuts-service';
 import {assertIsDefined} from '../utils/common-util';
 import {ConfigModel, configModelToken} from '../models/config/config-model';
 import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
@@ -59,16 +62,6 @@
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new UserModel(ctx.restApiService);
     },
-    shortcutsService: (ctx: Partial<AppContext>) => {
-      assertIsDefined(ctx.userModel, 'userModel');
-      assertIsDefined(ctx.reportingService, 'reportingService');
-      assertIsDefined(ctx.flagsService, 'flagsService');
-      return new ShortcutsService(
-        ctx.userModel,
-        ctx.flagsService,
-        ctx.reportingService
-      );
-    },
     pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
     highlightService: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.reportingService, 'reportingService');
@@ -112,5 +105,12 @@
 
   dependencies.set(checksModelToken, checksModel);
 
+  const shortcutsService = new ShortcutsService(
+    appContext.userModel,
+    appContext.flagsService,
+    appContext.reportingService
+  );
+  dependencies.set(shortcutsServiceToken, shortcutsService);
+
   return dependencies;
 }
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index cab34ba..3e1017d 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -13,7 +13,6 @@
 import {StorageService} from './storage/gr-storage';
 import {UserModel} from '../models/user/user-model';
 import {RouterModel} from './router/router-model';
-import {ShortcutsService} from './shortcuts/shortcuts-service';
 import {PluginsModel} from '../models/plugins/plugins-model';
 import {HighlightService} from './highlight/highlight-service';
 
@@ -27,7 +26,6 @@
   jsApiService: JsApiService;
   storageService: StorageService;
   userModel: UserModel;
-  shortcutsService: ShortcutsService;
   pluginsModel: PluginsModel;
   highlightService: HighlightService;
 }
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
index 6f13117..2996e99 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -6,7 +6,7 @@
 
 /** Enum for all special shortcuts */
 import {ComboKey, Key, Modifier, Binding} from '../../utils/dom-util';
-import {FlagsService} from '../flags/flags';
+import {FlagsService, KnownExperimentId} from '../flags/flags';
 
 export enum SPECIAL_SHORTCUT {
   DOC_ONLY = 'DOC_ONLY',
@@ -101,6 +101,8 @@
   SEND_REPLY = 'SEND_REPLY',
   EMOJI_DROPDOWN = 'EMOJI_DROPDOWN',
   TOGGLE_BLAME = 'TOGGLE_BLAME',
+
+  TOGGLE_CHECKBOX = 'TOGGLE_CHECKBOX',
 }
 
 export interface ShortcutHelpItem {
@@ -109,7 +111,7 @@
   bindings: Binding[];
 }
 
-export function createShortCutConfig(_flagsService: FlagsService) {
+export function createShortCutConfig(flagsService: FlagsService) {
   const config = new Map<ShortcutSection, ShortcutHelpItem[]>();
   function describe(
     shortcut: Shortcut,
@@ -165,6 +167,16 @@
     {key: 'w', combo: ComboKey.G}
   );
 
+  if (flagsService.isEnabled(KnownExperimentId.BULK_ACTIONS)) {
+    describe(
+      Shortcut.TOGGLE_CHECKBOX,
+      ShortcutSection.ACTIONS,
+      'Toggle checkbox',
+      {
+        key: 'x',
+      }
+    );
+  }
   describe(
     Shortcut.CURSOR_NEXT_CHANGE,
     ShortcutSection.ACTIONS,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 4232699..c27da15 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -24,6 +24,7 @@
 import {Finalizable} from '../registry';
 import {UserModel} from '../../models/user/user-model';
 import {FlagsService} from '../flags/flags';
+import {define} from '../../models/dependency';
 
 export type SectionView = Array<{binding: string[][]; text: string}>;
 
@@ -52,6 +53,9 @@
 
 export const COMBO_TIMEOUT_MS = 1000;
 
+export const shortcutsServiceToken =
+  define<ShortcutsService>('shortcuts-service');
+
 /**
  * Shortcuts service, holds all hosts, bindings and listeners.
  */
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 1492262..fb6b482 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -148,8 +148,6 @@
   // tests.
   initGlobalVariables(appContext);
 
-  const shortcuts = appContext.shortcutsService;
-  assert.isTrue(shortcuts._testOnly_isEmpty());
   const selection = document.getSelection();
   if (selection) {
     selection.removeAllRanges();
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index ed5d5be..8c8e46b 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -24,7 +24,10 @@
   commentsModelToken,
 } from '../models/comments/comments-model';
 import {RouterModel} from '../services/router/router-model';
-import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
+import {
+  ShortcutsService,
+  shortcutsServiceToken,
+} from '../services/shortcuts/shortcuts-service';
 import {ConfigModel, configModelToken} from '../models/config/config-model';
 import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
 import {PluginsModel} from '../models/plugins/plugins-model';
@@ -115,5 +118,13 @@
 
   dependencies.set(checksModelToken, checksModelCreator);
 
+  const shortcutServiceCreator = () =>
+    new ShortcutsService(
+      appContext.userModel,
+      appContext.flagsService,
+      appContext.reportingService
+    );
+  dependencies.set(shortcutsServiceToken, shortcutServiceCreator);
+
   return dependencies;
 }
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 82b3c00..b53d767 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -13,7 +13,6 @@
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
 import {UserModel} from '../models/user/user-model';
-import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
 import {queryAndAssert, query} from '../utils/common-util';
 import {FlagsService} from '../services/flags/flags';
 import {Key, Modifier} from '../utils/dom-util';
@@ -113,10 +112,6 @@
   return sinon.stub(getAppContext().userModel, method);
 }
 
-export function stubShortcuts<K extends keyof ShortcutsService>(method: K) {
-  return sinon.stub(getAppContext().shortcutsService, method);
-}
-
 export function stubHighlightService<K extends keyof HighlightService>(
   method: K
 ) {