Merge changes If813a507,Idb2695c7,If4aa216e,I373ce01e,Id85e47b4, ...
* changes:
Fix template types in gr-comment-thread
Fix template types in gr-comment
Fix template types in gr-dropdown-list
Fix template types in gr-hovercard-account
Fix template types in gr-label-info
Fix template types in gr-list-view
Add lit-analyzer and ts-lit-plugin, and fix most of the detected issues
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
index 9eb9b07..2d3b5c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
@@ -28,7 +28,6 @@
PluginConfigOptionsChangedEventDetail,
ArrayPluginOption,
} from '../gr-repo-plugin-config/gr-repo-plugin-config-types';
-import {KeydownEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
@@ -75,7 +74,7 @@
this._handleAdd();
}
- _handleInputKeydown(e: KeydownEvent) {
+ _handleInputKeydown(e: KeyboardEvent) {
// Enter.
if (e.keyCode === 13) {
e.preventDefault();
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 a2a46e0..ac44908 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
@@ -46,9 +46,9 @@
PreferencesInput,
} from '../../../types/common';
import {hasAttention} from '../../../utils/attention-set-util';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {fireEvent, fireReload} from '../../../utils/event-util';
-import {isShiftPressed} from '../../../utils/dom-util';
+import {isShiftPressed, modifierPressed} from '../../../utils/dom-util';
import {ScrollMode} from '../../../constants/constants';
const NUMBER_FIXED_COLUMNS = 3;
@@ -157,6 +157,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
override keyboardShortcuts() {
return {
[Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
@@ -176,9 +178,7 @@
super();
this.cursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.cursor.focusOnMove = true;
- this.addEventListener('keydown', e =>
- this._scopedKeydownHandler(e as unknown as CustomKeyboardEvent)
- );
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
}
override ready() {
@@ -210,10 +210,10 @@
*
* Context: Issue 7294
*/
- _scopedKeydownHandler(e: CustomKeyboardEvent) {
+ _scopedKeydownHandler(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter.
- this._openChange(e);
+ this.openChange(e);
}
}
@@ -406,8 +406,8 @@
);
}
- _nextChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _nextChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -418,8 +418,8 @@
this.selectedIndex = this.cursor.index;
}
- _prevChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _prevChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -430,19 +430,21 @@
this.selectedIndex = this.cursor.index;
}
- _openChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
+ _openChange(e: IronKeyboardEvent) {
+ if (this.modifierPressed(e)) return;
+ this.openChange(e.detail.keyboardEvent);
+ }
+ openChange(e: KeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || modifierPressed(e)) return;
e.preventDefault();
const change = this._changeForIndex(this.selectedIndex);
if (change) GerritNav.navigateToChange(change);
}
- _nextPage(e: CustomKeyboardEvent) {
+ _nextPage(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e))
) {
return;
@@ -452,9 +454,9 @@
fireEvent(this, 'next-page');
}
- _prevPage(e: CustomKeyboardEvent) {
+ _prevPage(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e))
) {
return;
@@ -469,8 +471,8 @@
);
}
- _toggleChangeReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _toggleChangeReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -488,8 +490,8 @@
changeEl.toggleReviewed();
}
- _refreshChangeList(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _refreshChangeList(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
@@ -497,8 +499,8 @@
fireReload(this);
}
- _toggleChangeStar(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _toggleChangeStar(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 7b226e7..4956380 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -19,8 +19,7 @@
import './gr-change-list.js';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {mockPromise, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
+import {mockPromise} from '../../../test/test-utils.js';
import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation.js';
const basicFixture = fixtureFromElement('gr-change-list');
@@ -28,22 +27,6 @@
suite('gr-change-list basic tests', () => {
let element;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
- kb.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
- kb.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
- kb.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(Shortcut.NEXT_PAGE, 'n');
- kb.bindShortcut(Shortcut.NEXT_PAGE, 'p');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
setup(() => {
element = basicFixture.instantiate();
});
@@ -495,11 +478,11 @@
assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
'Should navigate to /c/4/');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ MockInteractions.keyUpOn(element, 82); // 'r'
const change = element._changeForIndex(element.selectedIndex);
assert.equal(change.reviewed, true,
'Should mark change as reviewed');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ MockInteractions.keyUpOn(element, 82); // 'r'
assert.equal(change.reviewed, false,
'Should mark change as unreviewed');
promise.resolve();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 08ad2bd..ba62ec3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
import '../../../styles/gr-change-metadata-shared-styles';
import '../../../styles/gr-change-view-integration-shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index a37daaa..26d1277 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -20,6 +20,9 @@
<style include="gr-change-metadata-shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: table;
@@ -94,7 +97,6 @@
max-width: 285px;
}
.metadata-title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
@@ -114,7 +116,7 @@
</style>
<gr-external-style id="externalStyle" name="change-metadata">
<div class="metadata-header">
- <h3 class="metadata-title">Change Info</h3>
+ <h3 class="metadata-title heading-3">Change Info</h3>
<gr-button link="" class="show-all-button" on-click="_onShowAllClick"
>[[_computeShowAllLabelText(_showAllSections)]]
<iron-icon
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index 414784c..725bb24 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../../shared/gr-label-info/gr-label-info';
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index d824d94..8161592 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: table;
@@ -104,7 +107,7 @@
padding-left: 0;
}
</style>
- <h3 class="metadata-title">Submit requirements</h3>
+ <h3 class="metadata-title heading-3">Submit requirements</h3>
<template is="dom-repeat" items="[[_requirements]]">
<gr-endpoint-decorator
class="submit-requirement-endpoints"
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 3396e13..fd7b5d1 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
@@ -48,6 +48,7 @@
import {
KeyboardShortcutMixin,
Shortcut,
+ ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {pluralize} from '../../../utils/string-util';
@@ -157,8 +158,9 @@
ParsedChangeInfo,
} from '../../../types/types';
import {
+ IronKeyboardEventListener,
CloseFixPreviewEvent,
- CustomKeyboardEvent,
+ IronKeyboardEvent,
EditableContentSaveEvent,
EventType,
OpenFixPreviewEvent,
@@ -536,7 +538,7 @@
@property({type: Boolean})
_showRobotCommentsButton = false;
- _throttledToggleChangeStar?: EventListener;
+ _throttledToggleChangeStar?: IronKeyboardEventListener;
@property({type: Boolean})
_showChecksTab = false;
@@ -563,6 +565,8 @@
private readonly commentsService = appContext.commentsService;
+ private readonly shortcuts = appContext.shortcutsService;
+
private replyDialogResizeObserver?: ResizeObserver;
override keyboardShortcuts() {
@@ -637,8 +641,8 @@
override connectedCallback() {
super.connectedCallback();
- this._throttledToggleChangeStar = throttleWrap(e =>
- this._handleToggleChangeStar(e as CustomKeyboardEvent)
+ this._throttledToggleChangeStar = throttleWrap<IronKeyboardEvent>(e =>
+ this._handleToggleChangeStar(e)
);
this._getServerConfig().then(config => {
this._serverConfig = config;
@@ -747,8 +751,8 @@
if (e.detail.fixApplied) fireReload(this);
}
- _handleToggleDiffMode(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleDiffMode(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1491,8 +1495,8 @@
return label;
}
- _handleOpenReplyDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenReplyDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
this._getLoggedIn().then(isLoggedIn => {
@@ -1506,8 +1510,8 @@
});
}
- _handleOpenDownloadDialogShortcut(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenDownloadDialogShortcut(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1515,8 +1519,8 @@
this._handleOpenDownloadDialog();
}
- _handleEditTopic(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleEditTopic(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1524,8 +1528,8 @@
this.$.metadata.editTopic();
}
- _handleOpenSubmitDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || !this._submitEnabled) {
+ _handleOpenSubmitDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || !this._submitEnabled) {
return;
}
@@ -1533,8 +1537,8 @@
this.$.actions.showSubmitDialog();
}
- _handleToggleAttentionSet(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleToggleAttentionSet(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
if (!this._change || !this._account?._account_id) return;
@@ -1575,8 +1579,8 @@
this._change = {...this._change};
}
- _handleDiffAgainstBase(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffAgainstBase(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1589,8 +1593,8 @@
GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
}
- _handleDiffBaseAgainstLeft(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffBaseAgainstLeft(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1603,8 +1607,8 @@
GerritNav.navigateToChange(this._change, this._patchRange.basePatchNum);
}
- _handleDiffAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1622,8 +1626,8 @@
);
}
- _handleDiffRightAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffRightAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1641,8 +1645,8 @@
);
}
- _handleDiffBaseAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffBaseAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1659,24 +1663,24 @@
GerritNav.navigateToChange(this._change, latestPatchNum);
}
- _handleRefreshChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleRefreshChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
e.preventDefault();
fireReload(this, true);
}
- _handleToggleChangeStar(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleChangeStar(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
this.$.changeStar.toggleStar();
}
- _handleUpToDashboard(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleUpToDashboard(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1684,8 +1688,8 @@
this._determinePageBack();
}
- _handleExpandAllMessages(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleExpandAllMessages(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1695,8 +1699,8 @@
}
}
- _handleCollapseAllMessages(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCollapseAllMessages(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1706,8 +1710,8 @@
}
}
- _handleOpenDiffPrefsShortcut(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenDiffPrefsShortcut(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -2644,6 +2648,10 @@
'#relatedChanges'
);
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 6668e15..a82fceb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -35,12 +35,7 @@
import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
-import {
- mockPromise,
- stubRestApi,
- TestKeyboardShortcutBinder,
-} from '../../../test/test-utils';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {mockPromise, stubRestApi} from '../../../test/test-utils';
import {
createAppElementChangeViewParams,
createApproval,
@@ -87,13 +82,17 @@
} from '../../../types/common';
import {
pressAndReleaseKeyOn,
+ keyUpOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {AppElementChangeViewParams} from '../../gr-app-types';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {
+ IronKeyboardEvent,
+ IronKeyboardEventDetail,
+} from '../../../types/events';
import {CommentThread, UIRobot} from '../../../utils/comment-util';
import {GerritView} from '../../../services/router/router-model';
import {ParsedChangeInfo} from '../../../types/types';
@@ -111,25 +110,6 @@
typeof GerritNav.navigateToChange
>;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.SEND_REPLY, 'ctrl+enter');
- kb.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r');
- kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
- kb.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
- kb.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(Shortcut.EDIT_TOPIC, 't');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
const ROBOT_COMMENTS_LIMIT = 10;
// TODO: should have a mock service to generate VALID fake data
@@ -423,8 +403,7 @@
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._handleDiffAgainstBase(new CustomEvent('') as CustomKeyboardEvent);
+ element._handleDiffAgainstBase(new CustomEvent('') as IronKeyboardEvent);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
@@ -440,10 +419,7 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._handleDiffAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
- );
+ element._handleDiffAgainstLatest(new CustomEvent('') as IronKeyboardEvent);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
@@ -460,9 +436,8 @@
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffBaseAgainstLeft(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -479,9 +454,8 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffRightAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -498,9 +472,8 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffBaseAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -524,20 +497,15 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
assert.isNotOk(element._change.attention_set);
await element._getLoggedIn();
await element.restApiService.getAccount();
- element._handleToggleAttentionSet(
- new CustomEvent('') as CustomKeyboardEvent
- );
+ element._handleToggleAttentionSet(new CustomEvent('') as IronKeyboardEvent);
assert.isTrue(addToAttentionSetStub.called);
assert.isFalse(removeFromAttentionSetStub.called);
- element._handleToggleAttentionSet(
- new CustomEvent('') as CustomKeyboardEvent
- );
+ element._handleToggleAttentionSet(new CustomEvent('') as IronKeyboardEvent);
assert.isTrue(removeFromAttentionSetStub.called);
});
@@ -682,7 +650,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- pressAndReleaseKeyOn(element, 65, null, 'a');
+ keyUpOn(element, 65, null, 'a');
await flush();
assert.isFalse(element.$.replyOverlay.opened);
assert.isTrue(loggedInErrorSpy.called);
@@ -715,7 +683,7 @@
const openSpy = sinon.spy(element, '_openReplyDialog');
- pressAndReleaseKeyOn(element, 65, null, 'a');
+ keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.$.replyOverlay.opened);
element.$.replyOverlay.close();
@@ -828,7 +796,7 @@
const stub = sinon
.stub(element.$.downloadOverlay, 'open')
.returns(Promise.resolve());
- pressAndReleaseKeyOn(element, 68, null, 'd');
+ keyUpOn(element, 68, null, 'd');
assert.isTrue(stub.called);
});
@@ -852,12 +820,13 @@
});
test('m should toggle diff mode', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const setModeStub = sinon.stub(
element.$.fileListHeader,
'setDiffViewMode'
);
- const e = {preventDefault: () => {}} as CustomKeyboardEvent;
+ const e = new CustomEvent<IronKeyboardEventDetail>('keydown', {
+ detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
+ });
flush();
element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
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 bdc6a43..8aef3c0 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
@@ -44,7 +44,12 @@
import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {
+ KeyboardShortcutMixin,
+ Shortcut,
+ ShortcutSection,
+} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -144,6 +149,8 @@
@property({type: Object})
revisionInfo?: RevisionInfo;
+ private readonly shortcuts = appContext.shortcutsService;
+
setDiffViewMode(mode: DiffViewMode) {
this.$.modeSelect.setMode(mode);
}
@@ -217,4 +224,8 @@
}
return 'patchInfoOldPatchSet';
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
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 40a2e1f..b78c78f 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
@@ -50,8 +50,8 @@
} from '../../../constants/constants';
import {
descendedFromClass,
- getKeyboardEvent,
isShiftPressed,
+ modifierPressed,
toggleClass,
} from '../../../utils/dom-util';
import {
@@ -78,7 +78,7 @@
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
@@ -356,14 +356,14 @@
private diffCursor = new GrDiffCursor();
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.fileCursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.fileCursor.cursorTargetClass = 'selected';
this.fileCursor.focusOnMove = true;
- this.addEventListener('keydown', e =>
- this._scopedKeydownHandler(e as unknown as CustomKeyboardEvent)
- );
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
}
override connectedCallback() {
@@ -433,13 +433,8 @@
*
* Context: Issue 7277
*/
- _scopedKeydownHandler(e: CustomKeyboardEvent) {
- if (e.keyCode === 13) {
- // TODO(TS): e is not an instance of CustomKeyboardEvent.
- // However, to fix it we should fix keyboard-shortcut-mixin first
- // The keyboard-shortcut-mixin will be updated in a separate change
- this._handleOpenFile(e as unknown as CustomKeyboardEvent);
- }
+ _scopedKeydownHandler(e: KeyboardEvent) {
+ if (e.keyCode === 13) this.handleOpenFile(e);
}
reload() {
@@ -886,8 +881,8 @@
return fileData;
}
- _handleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ _handleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this._noDiffsExpanded()) {
return;
}
@@ -895,8 +890,8 @@
this.diffCursor.moveLeft();
}
- _handleRightPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ _handleRightPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this._noDiffsExpanded()) {
return;
}
@@ -904,9 +899,9 @@
this.diffCursor.moveRight();
}
- _handleToggleInlineDiff(e: CustomKeyboardEvent) {
+ _handleToggleInlineDiff(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
this.modifierPressed(e) ||
e.detail?.keyboardEvent?.repeat ||
this.fileCursor.index === -1
@@ -918,11 +913,8 @@
this._toggleFileExpandedByIndex(this.fileCursor.index);
}
- _handleToggleAllInlineDiffs(e: CustomKeyboardEvent) {
- if (
- this.shouldSuppressKeyboardShortcut(e) ||
- e.detail?.keyboardEvent?.repeat
- ) {
+ _handleToggleAllInlineDiffs(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail?.keyboardEvent?.repeat) {
return;
}
@@ -930,8 +922,8 @@
this._toggleInlineDiffs();
}
- _handleToggleHideAllCommentThreads(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleHideAllCommentThreads(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -939,8 +931,8 @@
toggleClass(this, 'hideComments');
}
- _handleCursorNext(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCursorNext(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -950,7 +942,7 @@
this._displayLine = true;
} else {
// Down key
- if (getKeyboardEvent(e).keyCode === 40) {
+ if (e.detail.keyboardEvent.keyCode === 40) {
return;
}
e.preventDefault();
@@ -959,8 +951,8 @@
}
}
- _handleCursorPrev(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCursorPrev(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -970,7 +962,7 @@
this._displayLine = true;
} else {
// Up key
- if (getKeyboardEvent(e).keyCode === 38) {
+ if (e.detail.keyboardEvent.keyCode === 38) {
return;
}
e.preventDefault();
@@ -979,8 +971,8 @@
}
}
- _handleNewComment(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleNewComment(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
@@ -988,9 +980,9 @@
this.diffCursor.createCommentInPlace();
}
- _handleOpenLastFile(e: CustomKeyboardEvent) {
+ _handleOpenLastFile(e: IronKeyboardEvent) {
// Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) || getKeyboardEvent(e).metaKey) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail.keyboardEvent.metaKey) {
return;
}
@@ -998,9 +990,9 @@
this._openSelectedFile(this._files.length - 1);
}
- _handleOpenFirstFile(e: CustomKeyboardEvent) {
+ _handleOpenFirstFile(e: IronKeyboardEvent) {
// Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) || getKeyboardEvent(e).metaKey) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail.keyboardEvent.metaKey) {
return;
}
@@ -1008,8 +1000,13 @@
this._openSelectedFile(0);
}
- _handleOpenFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenFile(e: IronKeyboardEvent) {
+ if (this.modifierPressed(e)) return;
+ this.handleOpenFile(e.detail.keyboardEvent);
+ }
+
+ handleOpenFile(e: KeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || modifierPressed(e)) {
return;
}
e.preventDefault();
@@ -1022,9 +1019,9 @@
this._openSelectedFile();
}
- _handleNextChunk(e: CustomKeyboardEvent) {
+ _handleNextChunk(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e)) ||
this._noDiffsExpanded()
) {
@@ -1039,9 +1036,9 @@
}
}
- _handlePrevChunk(e: CustomKeyboardEvent) {
+ _handlePrevChunk(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e)) ||
this._noDiffsExpanded()
) {
@@ -1056,8 +1053,8 @@
}
}
- _handleToggleFileReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleFileReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1068,8 +1065,8 @@
this._reviewFile(this._files[this.fileCursor.index].__path);
}
- _handleToggleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleToggleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
@@ -1545,8 +1542,8 @@
return undefined;
}
- _handleEscKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleEscKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index b8c6cde..f4064da 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -27,7 +27,6 @@
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {
- TestKeyboardShortcutBinder,
stubRestApi,
spyRestApi,
listenOnce,
@@ -35,7 +34,6 @@
query,
} from '../../../test/test-utils.js';
import {EditPatchSetNum} from '../../../types/common.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {createCommentThreads} from '../../../utils/comment-util.js';
import {
createChange,
@@ -68,30 +66,6 @@
let saveStub;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- kb.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- kb.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- kb.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
- kb.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
- kb.bindShortcut(Shortcut.OPEN_FILE, 'o');
- kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
suite('basic tests', () => {
setup(async () => {
stubRestApi('getDiffComments').returns(Promise.resolve({}));
@@ -566,27 +540,27 @@
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[0]);
assert.equal(element._expandedFiles.length, 1);
assert.equal(element._expandedFiles[0].path, paths[0]);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
element.fileCursor.setCursorAtIndex(1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[1]);
assert.equal(element._expandedFiles.length, 1);
assert.equal(element._expandedFiles[0].path, paths[1]);
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
flush();
assert.equal(element.diffs.length, paths.length);
assert.equal(element._expandedFiles.length, paths.length);
@@ -595,7 +569,7 @@
}
// since _expandedFilesChanged is stubbed
element.filesExpanded = FilesExpandedState.ALL;
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
flush();
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
@@ -610,12 +584,12 @@
assert.equal(getNumReviewed(), 0);
// Press the review key to toggle it (set the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
flush();
assert.equal(getNumReviewed(), 1);
// Press the review key to toggle it (clear the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.equal(getNumReviewed(), 0);
});
@@ -623,22 +597,23 @@
let interact;
setup(() => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut')
- .returns(false);
sinon.stub(element, 'modifierPressed').returns(false);
const openCursorStub = sinon.stub(element, '_openCursorFile');
const openSelectedStub = sinon.stub(element, '_openSelectedFile');
const expandStub = sinon.stub(element, '_toggleFileExpanded');
- interact = function(opt_payload) {
+ interact = function() {
openCursorStub.reset();
openSelectedStub.reset();
expandStub.reset();
- const e = new CustomEvent('fake-keyboard-event', opt_payload);
- sinon.stub(e, 'preventDefault');
+ const keyboardEvent = new KeyboardEvent('keydown');
+ const e = new CustomEvent('keydown', {
+ detail: {keyboardEvent, key: 'x'},
+ });
+ sinon.stub(keyboardEvent, 'preventDefault');
element._handleOpenFile(e);
- assert.isTrue(e.preventDefault.called);
+ assert.isTrue(keyboardEvent.preventDefault.called);
const result = {};
if (openCursorStub.called) {
result.opened_cursor = true;
@@ -1574,7 +1549,7 @@
});
test('cursor with individually opened files', async () => {
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
await flush();
let diffs = await renderAndGetNewDiffs(0);
const diffStops = diffs[0].getCursorStops();
@@ -1601,7 +1576,7 @@
// The file cursor is now at 1.
assert.equal(element.fileCursor.index, 1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
await flush();
diffs = await renderAndGetNewDiffs(1);
@@ -1616,7 +1591,7 @@
});
test('cursor with toggle all files', async () => {
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
await flush();
const diffs = await renderAndGetNewDiffs(0);
@@ -1663,7 +1638,7 @@
});
test('n key with some files expanded and no shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
await flush();
// Handle N key should return before calling diff cursor functions.
@@ -1677,7 +1652,7 @@
});
test('n key with some files expanded and shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
await flush();
assert.equal(nextChunkStub.callCount, 0);
@@ -1691,7 +1666,7 @@
});
test('n key without all files expanded and shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
await flush();
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
@@ -1704,7 +1679,7 @@
});
test('n key without all files expanded and no shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
await flush();
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
@@ -1733,12 +1708,13 @@
});
test('_displayLine', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut')
- .callsFake(() => false);
sinon.stub(element, 'modifierPressed')
.callsFake(() => false);
element.filesExpanded = FilesExpandedState.ALL;
- const mockEvent = {preventDefault() {}};
+ const mockEvent = {
+ preventDefault() {},
+ composedPath() { return []; },
+ };
element._displayLine = false;
element._handleCursorNext(mockEvent);
@@ -1759,13 +1735,13 @@
const saveReviewStub = sinon.stub(element, '_saveReviewedState');
element.editMode = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(saveReviewStub.calledOnce);
element.editMode = true;
await flush();
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(saveReviewStub.calledOnce);
});
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 fdd7c79..cd79bba 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
@@ -257,6 +257,8 @@
private readonly reporting = appContext.reportingService;
+ private readonly shortcuts = appContext.shortcutsService;
+
scrollToMessage(messageID: string) {
const selector = `[data-message-id="${messageID}"]`;
const el = this.shadowRoot!.querySelector(selector) as
@@ -384,13 +386,13 @@
_computeExpandAllTitle(_expandAllState?: string) {
if (_expandAllState === ExpandAllState.COLLAPSE_ALL) {
- return this.createTitle(
+ return this.shortcuts.createTitle(
Shortcut.COLLAPSE_ALL_MESSAGES,
ShortcutSection.ACTIONS
);
}
if (_expandAllState === ExpandAllState.EXPAND_ALL) {
- return this.createTitle(
+ return this.shortcuts.createTitle(
Shortcut.EXPAND_ALL_MESSAGES,
ShortcutSection.ACTIONS
);
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 74f20f2..bce4024 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
@@ -40,6 +40,7 @@
isChangeInfo,
} from '../../../utils/change-util';
import {Interaction} from '../../../constants/reporting';
+import {fontStyles} from '../../../styles/gr-font-styles';
/** What is the maximum number of shown changes in collapsed list? */
const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
@@ -686,9 +687,9 @@
static override get styles() {
return [
sharedStyles,
+ fontStyles,
css`
.title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
@@ -720,7 +721,7 @@
}
override render() {
- const title = html`<h4 class="title">${this.title}</h4>`;
+ const title = html`<h3 class="title heading-3">${this.title}</h3>`;
const collapsible = this.length > this.numChangesWhenCollapsed;
this.collapsed = !this.showAll && collapsible;
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 934d61a..19bc388 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -61,7 +61,6 @@
fontStyles,
css`
.metadata-title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
margin: 0 0 var(--spacing-s);
@@ -129,12 +128,12 @@
const submit_requirements = (this.change?.submit_requirements ?? []).filter(
req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
);
- return html` <h2
+ return html` <h3
class="metadata-title heading-3"
id="submit-requirements-caption"
>
Submit Requirements
- </h2>
+ </h3>
<table class="requirements" aria-labelledby="submit-requirements-caption">
<thead hidden>
<tr>
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 0b191f1..541d877 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
@@ -27,6 +27,7 @@
SectionView,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {property, customElement} from '@polymer/decorators';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -60,13 +61,14 @@
@property({type: Array})
_right?: SectionShortcut[];
- private keyboardShortcutDirectoryListener: ShortcutListener;
+ private readonly shortcutListener: ShortcutListener;
+
+ private readonly shortcuts = appContext.shortcutsService;
constructor() {
super();
- this.keyboardShortcutDirectoryListener = (
- d?: Map<ShortcutSection, SectionView>
- ) => this._onDirectoryUpdated(d);
+ this.shortcutListener = (d?: Map<ShortcutSection, SectionView>) =>
+ this._onDirectoryUpdated(d);
}
override ready() {
@@ -76,15 +78,11 @@
override connectedCallback() {
super.connectedCallback();
- this.addKeyboardShortcutDirectoryListener(
- this.keyboardShortcutDirectoryListener
- );
+ this.shortcuts.addListener(this.shortcutListener);
}
override disconnectedCallback() {
- this.removeKeyboardShortcutDirectoryListener(
- this.keyboardShortcutDirectoryListener
- );
+ this.shortcuts.removeListener(this.shortcutListener);
super.disconnectedCallback();
}
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 240bb22..78c1ebd 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -31,10 +31,9 @@
GrAutocomplete,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getDocsBaseUrl} from '../../../utils/url-util';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {MergeabilityComputationBehavior} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
-import {getKeyboardEvent} from '../../../utils/dom-util';
// Possible static search options for auto complete, without negations.
const SEARCH_OPERATORS: ReadonlyArray<string> = [
@@ -199,6 +198,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.query = (input: string) => this._getSearchSuggestions(input);
@@ -396,10 +397,10 @@
});
}
- _handleSearch(e: CustomKeyboardEvent) {
- const keyboardEvent = getKeyboardEvent(e);
+ _handleSearch(e: IronKeyboardEvent) {
+ const keyboardEvent = e.detail.keyboardEvent;
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !keyboardEvent.shiftKey)
) {
return;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index b5b0124..b6d0579 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -19,12 +19,7 @@
import './gr-search-bar';
import '../../../scripts/util';
import {GrSearchBar} from './gr-search-bar';
-import {
- TestKeyboardShortcutBinder,
- stubRestApi,
- mockPromise,
-} from '../../../test/test-utils';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {stubRestApi, mockPromise} from '../../../test/test-utils';
import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {
@@ -39,15 +34,6 @@
suite('gr-search-bar tests', () => {
let element: GrSearchBar;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.SEARCH, '/');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
setup(async () => {
element = basicFixture.instantiate();
await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
index 5e81871..79e7dbc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
@@ -134,7 +134,7 @@
if (node instanceof Text) {
this._annotateText(node, offset, subLength, cssClass);
- } else if (node instanceof HTMLElement) {
+ } else if (node instanceof Element) {
this.annotateElement(node, offset, subLength, cssClass);
}
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 bb5ce94..e2baec9 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
@@ -37,6 +37,7 @@
import {
KeyboardShortcutMixin,
Shortcut,
+ ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {
GeneratedWebLink,
@@ -98,14 +99,15 @@
} from '../../../utils/comment-util';
import {AppElementParams} from '../../gr-app-types';
import {
- CustomKeyboardEvent,
+ IronKeyboardEventListener,
+ IronKeyboardEvent,
EventType,
OpenFixPreviewEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
-import {toggleClass, getKeyboardEvent} from '../../../utils/dom-util';
+import {toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
import {throttleWrap} from '../../../utils/async-util';
import {changeComments$} from '../../../services/comments/comments-model';
@@ -341,7 +343,9 @@
private readonly commentsService = appContext.commentsService;
- _throttledToggleFileReviewed?: EventListener;
+ private readonly shortcuts = appContext.shortcutsService;
+
+ _throttledToggleFileReviewed?: IronKeyboardEventListener;
_onRenderHandler?: EventListener;
@@ -352,7 +356,7 @@
override connectedCallback() {
super.connectedCallback();
this._throttledToggleFileReviewed = throttleWrap(e =>
- this._handleToggleFileReviewed(e as CustomKeyboardEvent)
+ this._handleToggleFileReviewed(e)
);
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
@@ -518,38 +522,38 @@
);
}
- _handleToggleFileReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleFileReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this._setReviewed(!this.$.reviewed.checked);
}
- _handleEscKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleEscKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this.$.diffHost.displayLine = false;
}
- _handleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveLeft();
}
- _handleRightPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleRightPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveRight();
}
- _handlePrevLineOrFileWithComments(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevLineOrFileWithComments(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (
e.detail.keyboardEvent?.shiftKey &&
@@ -568,8 +572,8 @@
this.cursor.moveUp();
}
- _handleVisibleLine(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleVisibleLine(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveToVisibleArea();
@@ -579,8 +583,8 @@
this.$.applyFixDialog.open(e);
}
- _handleNextLineOrFileWithComments(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextLineOrFileWithComments(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (
e.detail.keyboardEvent?.shiftKey &&
@@ -638,39 +642,41 @@
);
}
- _handleNewComment(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
- if (this.modifierPressed(e)) return;
+ _handleNewComment(ike: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(ike)) return;
+ if (this.modifierPressed(ike)) return;
- e.preventDefault();
+ ike.preventDefault();
this.classList.remove('hideComments');
this.cursor.createCommentInPlace();
}
- _handlePrevFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevFile(ike: IronKeyboardEvent) {
+ const ke = ike.detail.keyboardEvent;
+ if (this.shortcuts.shouldSuppress(ike)) return;
// Check for meta key to avoid overriding native chrome shortcut.
- if (getKeyboardEvent(e).metaKey) return;
+ if (ke.metaKey) return;
if (!this._path) return;
if (!this._fileList) return;
- e.preventDefault();
+ ike.preventDefault();
this._navToFile(this._path, this._fileList, -1);
}
- _handleNextFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextFile(ike: IronKeyboardEvent) {
+ const ke = ike.detail.keyboardEvent;
+ if (this.shortcuts.shouldSuppress(ike)) return;
// Check for meta key to avoid overriding native chrome shortcut.
- if (getKeyboardEvent(e).metaKey) return;
+ if (ke.metaKey) return;
if (!this._path) return;
if (!this._fileList) return;
- e.preventDefault();
+ ike.preventDefault();
this._navToFile(this._path, this._fileList, 1);
}
- _handleNextChunkOrCommentThread(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextChunkOrCommentThread(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
@@ -730,8 +736,8 @@
this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
}
- _handlePrevChunkOrCommentThread(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevChunkOrCommentThread(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
@@ -746,8 +752,8 @@
}
// Similar to gr-change-view._handleOpenReplyDialog
- _handleOpenReplyDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenReplyDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this._getLoggedIn().then(isLoggedIn => {
if (!isLoggedIn) {
@@ -761,16 +767,16 @@
});
}
- _handleToggleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!e.detail.keyboardEvent?.shiftKey) return;
e.preventDefault();
this.$.diffHost.toggleLeftDiff();
}
- _handleOpenDownloadDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenDownloadDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this.set('changeViewState.showDownloadDialog', true);
@@ -778,16 +784,16 @@
this._navToChangeView();
}
- _handleUpToChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleUpToChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this._navToChangeView();
}
- _handleCommaKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleCommaKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
if (this._diffPrefsDisabled) return;
@@ -795,8 +801,8 @@
this.$.diffPreferencesDialog.open();
}
- _handleToggleDiffMode(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleDiffMode(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
@@ -1692,28 +1698,28 @@
this._loadBlame();
}
- _handleToggleBlame(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleBlame(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this._toggleBlame();
}
- _handleToggleHideAllCommentThreads(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleHideAllCommentThreads(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
toggleClass(this, 'hideComments');
}
- _handleOpenFileList(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenFileList(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this.$.dropdown.open();
}
- _handleDiffAgainstBase(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffAgainstBase(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1729,8 +1735,8 @@
);
}
- _handleDiffBaseAgainstLeft(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffBaseAgainstLeft(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1750,8 +1756,8 @@
);
}
- _handleDiffAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1770,8 +1776,8 @@
);
}
- _handleDiffRightAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffRightAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1789,8 +1795,8 @@
);
}
- _handleDiffBaseAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffBaseAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1827,8 +1833,8 @@
return '';
}
- _handleToggleAllDiffContext(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleAllDiffContext(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this.$.diffHost.toggleAllContext();
}
@@ -1837,8 +1843,8 @@
return disableDiffPrefs || !loggedIn;
}
- _handleNextUnreviewedFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextUnreviewedFile(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._setReviewed(true);
this.navigateToUnreviewedFile('next');
}
@@ -1898,6 +1904,10 @@
_computeTruncatedPath(path?: string) {
return path ? computeTruncatedPath(path) : '';
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
declare global {
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 a3de30a..735624a 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
@@ -19,8 +19,7 @@
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus} from '../../../constants/constants.js';
-import {TestKeyboardShortcutBinder, stubRestApi} from '../../../test/test-utils.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
+import {stubRestApi} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
@@ -41,42 +40,6 @@
let clock;
let diffCommentsStub;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- kb.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(Shortcut.SAVE_COMMENT, 'ctrl+s');
- kb.bindShortcut(Shortcut.NEXT_FILE, ']');
- kb.bindShortcut(Shortcut.PREV_FILE, '[');
- kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
- kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
- kb.bindShortcut(Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
- kb.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
- kb.bindShortcut(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
- kb.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- kb.bindShortcut(Shortcut.TOGGLE_BLAME, 'b');
- kb.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
const PARENT = 'PARENT';
function getFilesFromFileList(fileList) {
@@ -504,16 +467,16 @@
sinon.stub(element, '_setReviewed');
sinon.spy(element, '_handleToggleFileReviewed');
element.$.reviewed.checked = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ MockInteractions.keyUpOn(element, 82, 'shift', 'r');
assert.isFalse(element._setReviewed.called);
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
clock.tick(1000);
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledTwice);
assert.isTrue(element._setReviewed.called);
assert.equal(element._setReviewed.lastCall.args[0], true);
@@ -573,7 +536,6 @@
basePatchNum: 5,
patchNum: 10,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstBase(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
@@ -590,7 +552,6 @@
basePatchNum: 5,
patchNum: 10,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstLatest(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
@@ -608,7 +569,6 @@
basePatchNum: 1,
};
element.params = {};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
@@ -631,7 +591,6 @@
sinon.stub(element, '_paramsChanged');
element.params = {commentLink: true, view: GerritView.DIFF};
element._focusLineNum = 10;
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
@@ -650,7 +609,6 @@
basePatchNum: 1,
patchNum: 3,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffRightAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
@@ -668,7 +626,6 @@
basePatchNum: 1,
patchNum: 3,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
@@ -682,7 +639,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
'should only work when the user is logged in.');
@@ -708,7 +665,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
@@ -734,7 +691,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
@@ -798,7 +755,7 @@
'Should navigate to /c/42/5..10');
assert.isUndefined(element.changeViewState.showDownloadDialog);
- MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
+ MockInteractions.keyUpOn(element, 68, null, 'd');
assert.isTrue(element.changeViewState.showDownloadDialog);
});
@@ -1520,8 +1477,9 @@
});
test('_handleToggleDiffMode', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const e = {preventDefault: () => {}};
+ const e = new CustomEvent('keydown', {
+ detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
+ });
// Initial state.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
@@ -1732,7 +1690,7 @@
test('toggle blame with shortcut', () => {
const toggleBlame = sinon.stub(
element.$.diffHost, 'loadBlame').callsFake(() => Promise.resolve());
- MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
+ MockInteractions.keyUpOn(element, 66, null, 'b');
assert.isTrue(toggleBlame.calledOnce);
});
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index f0a3ca1..d87b573 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -351,7 +351,7 @@
}
}
- _handleKeyPress(event: InputEvent) {
+ _handleKeyPress(event: KeyboardEvent) {
event.preventDefault();
event.stopImmediatePropagation();
}
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 6f2d27e..ad7e015 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -47,6 +47,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDefaultEditor} from '../gr-default-editor/gr-default-editor';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import {IronKeyboardEvent} from '../../../types/events';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -393,7 +394,7 @@
);
}
- _handleSaveShortcut(e: KeyboardEvent) {
+ _handleSaveShortcut(e: IronKeyboardEvent) {
e.preventDefault();
if (!this._saveDisabled) {
this._saveEdit();
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index b6fe60b..3b93bea 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -43,7 +43,6 @@
import {
KeyboardShortcutMixin,
Shortcut,
- SPECIAL_SHORTCUT,
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GerritNav} from './core/gr-navigation/gr-navigation';
import {appContext} from '../services/app-context';
@@ -69,7 +68,7 @@
import {GrMainHeader} from './core/gr-main-header/gr-main-header';
import {GrSettingsView} from './settings/gr-settings-view/gr-settings-view';
import {
- CustomKeyboardEvent,
+ IronKeyboardEvent,
DialogChangeEventDetail,
EventType,
LocationChangeEvent,
@@ -215,6 +214,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
override keyboardShortcuts() {
return {
[Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
@@ -232,7 +233,6 @@
// model changes and updates the config model, but at the moment the service
// is not called from anywhere.
appContext.configService;
- this._bindKeyboardShortcuts();
document.addEventListener(EventType.PAGE_ERROR, e => {
this._handlePageError(e);
});
@@ -307,159 +307,6 @@
};
}
- _bindKeyboardShortcuts() {
- this.bindShortcut(
- Shortcut.SEND_REPLY,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'ctrl+enter',
- 'meta+enter'
- );
- this.bindShortcut(Shortcut.EMOJI_DROPDOWN, SPECIAL_SHORTCUT.DOC_ONLY, ':');
-
- this.bindShortcut(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
- this.bindShortcut(
- Shortcut.GO_TO_USER_DASHBOARD,
- SPECIAL_SHORTCUT.GO_KEY,
- 'i'
- );
- this.bindShortcut(
- Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'o'
- );
- this.bindShortcut(
- Shortcut.GO_TO_MERGED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'm'
- );
- this.bindShortcut(
- Shortcut.GO_TO_ABANDONED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'a'
- );
- this.bindShortcut(
- Shortcut.GO_TO_WATCHED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'w'
- );
-
- this.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
- this.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
- this.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
- this.bindShortcut(Shortcut.NEXT_PAGE, 'n', ']');
- this.bindShortcut(Shortcut.PREV_PAGE, 'p', '[');
- this.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
- this.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's:keydown');
- this.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
- this.bindShortcut(Shortcut.EDIT_TOPIC, 't');
- this.bindShortcut(Shortcut.OPEN_SUBMIT_DIALOG, 'shift+s');
- this.bindShortcut(Shortcut.TOGGLE_ATTENTION_SET, 'shift+t');
-
- this.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
- this.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
- this.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
- this.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- this.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
- this.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
- this.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
- this.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
- this.bindShortcut(
- Shortcut.DIFF_AGAINST_BASE,
- SPECIAL_SHORTCUT.V_KEY,
- 'down',
- 's'
- );
- // this keyboard shortcut is used in toast _displayDiffAgainstLatestToast
- // in gr-diff-view. Any updates here should be reflected there
- this.bindShortcut(
- Shortcut.DIFF_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'up',
- 'w'
- );
- // this keyboard shortcut is used in toast _displayDiffBaseAgainstLeftToast
- // in gr-diff-view. Any updates here should be reflected there
- this.bindShortcut(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- SPECIAL_SHORTCUT.V_KEY,
- 'left',
- 'a'
- );
- this.bindShortcut(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'right',
- 'd'
- );
- this.bindShortcut(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'b'
- );
-
- this.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- this.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- if (this._isCursorManagerSupportMoveToVisibleLine()) {
- this.bindShortcut(Shortcut.VISIBLE_LINE, '.');
- }
- this.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- this.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- this.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
- this.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- this.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- this.bindShortcut(
- Shortcut.EXPAND_ALL_COMMENT_THREADS,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'e'
- );
- this.bindShortcut(
- Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'shift+e'
- );
- this.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- this.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- this.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- this.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- this.bindShortcut(
- Shortcut.SAVE_COMMENT,
- 'ctrl+enter',
- 'meta+enter',
- 'ctrl+s',
- 'meta+s'
- );
- this.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- this.bindShortcut(Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
-
- this.bindShortcut(Shortcut.NEXT_FILE, ']');
- this.bindShortcut(Shortcut.PREV_FILE, '[');
- this.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- this.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- this.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- this.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- this.bindShortcut(Shortcut.OPEN_FILE, 'o', 'enter');
- this.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
- this.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- this.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i');
- this.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i');
- this.bindShortcut(Shortcut.TOGGLE_BLAME, 'b:keyup');
- this.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
- this.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
-
- this.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
- this.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
-
- this.bindShortcut(Shortcut.SEARCH, '/');
- }
-
- _isCursorManagerSupportMoveToVisibleLine() {
- // This method is a copy-paste from the
- // method _isIntersectionObserverSupported of gr-cursor-manager.js
- // It is better share this method with gr-cursor-manager,
- // but doing it require a lot if changes instead of 1-line copied code
- return 'IntersectionObserver' in window;
- }
-
_accountChanged(account?: AccountDetailInfo) {
if (!account) return;
@@ -655,7 +502,8 @@
(this.shadowRoot!.querySelector('#keyboardShortcuts') as GrOverlay).open();
}
- _showKeyboardShortcuts(e: CustomKeyboardEvent) {
+ _showKeyboardShortcuts(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
// same shortcut should close the dialog if pressed again
// when dialog is open
this.loadKeyboardShortcutsDialog = true;
@@ -668,9 +516,6 @@
keyboardShortcuts.cancel();
return;
}
- if (this.shouldSuppressKeyboardShortcut(e)) {
- return;
- }
keyboardShortcuts.open();
this._footerHeaderAriaHidden = true;
this._mainAriaHidden = true;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 536beb4..bd6835c 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -26,7 +26,6 @@
import {EditableAccountField} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
-import {KeydownEvent} from '../../../types/events';
@customElement('gr-account-info')
export class GrAccountInfo extends PolymerElement {
@@ -247,7 +246,7 @@
this._hasNameChange = true;
}
- _handleKeydown(e: KeydownEvent) {
+ _handleKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index 4096b02..c392a13 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -23,7 +23,6 @@
import {htmlTemplate} from './gr-menu-editor_html';
import {customElement, property} from '@polymer/decorators';
import {TopMenuItemInfo} from '../../../types/common';
-import {KeydownEvent} from '../../../types/events';
@customElement('gr-menu-editor')
export class GrMenuEditor extends PolymerElement {
@@ -90,7 +89,7 @@
return !newName?.length || !newUrl?.length;
}
- _handleInputKeydown(e: KeydownEvent) {
+ _handleInputKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
e.stopPropagation();
this._handleAddButton();
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 94333c7..3533fd6 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
@@ -62,7 +62,6 @@
import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
-import {KeydownEvent} from '../../../types/events';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {GerritView} from '../../../services/router/router-model';
@@ -489,7 +488,7 @@
this.$.emailEditor.save();
}
- _handleNewEmailKeydown(e: KeydownEvent) {
+ _handleNewEmailKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index c5d1f03..d97e38e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -39,7 +39,6 @@
import {PaperInputElementExt} from '../../../types/types';
import {fireAlert} from '../../../utils/event-util';
import {accountOrGroupKey} from '../../../utils/account-util';
-import {KeydownEvent} from '../../../types/events';
const VALID_EMAIL_ALERT = 'Please input a valid email.';
@@ -360,7 +359,7 @@
}
}
- _handleChipKeydown(e: KeydownEvent) {
+ _handleChipKeydown(e: KeyboardEvent) {
const chip = e.target as GrAccountChip;
const chips = this.accountChips;
const index = chips.indexOf(chip);
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 9918f39..524b197 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -26,10 +26,10 @@
import {property, customElement, observe} from '@polymer/decorators';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElementExt} from '../../../types/types';
-import {CustomKeyboardEvent} from '../../../types/events';
import {fireEvent} from '../../../utils/event-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {PropertyType} from '../../../types/common';
+import {modifierPressed} from '../../../utils/dom-util';
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
const DEBOUNCE_WAIT_MS = 200;
@@ -358,7 +358,7 @@
* _handleKeydown used for key handling in the this.$.input AND all child
* autocomplete options.
*/
- _handleKeydown(e: CustomKeyboardEvent) {
+ _handleKeydown(e: KeyboardEvent) {
this._focused = true;
switch (e.keyCode) {
case 38: // Up
@@ -383,7 +383,7 @@
}
break;
case 13: // Enter
- if (this.modifierPressed(e)) {
+ if (modifierPressed(e)) {
break;
}
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 7017c9c..8dc23e2 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -19,14 +19,9 @@
import {votingStyles} from '../../../styles/gr-voting-styles';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators';
-import {
- getEventPath,
- getKeyboardEvent,
- isModifierPressed,
-} from '../../../utils/dom-util';
+import {getEventPath, modifierPressed} from '../../../utils/dom-util';
import {appContext} from '../../../services/app-context';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
-import {CustomKeyboardEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
@@ -206,9 +201,7 @@
super();
this.initialTabindex = this.getAttribute('tabindex') || '0';
this.addEventListener('click', e => this._handleAction(e));
- this.addEventListener('keydown', e =>
- this._handleKeydown(e as unknown as CustomKeyboardEvent)
- );
+ this.addEventListener('keydown', e => this._handleKeydown(e));
}
override updated(changedProperties: PropertyValues) {
@@ -247,11 +240,8 @@
this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
}
- _handleKeydown(e: CustomKeyboardEvent) {
- if (isModifierPressed(e)) {
- return;
- }
- e = getKeyboardEvent(e);
+ _handleKeydown(e: KeyboardEvent) {
+ if (modifierPressed(e)) return;
// Handle `enter`, `space`.
if (e.keyCode === 13 || e.keyCode === 32) {
e.preventDefault();
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 5069ba4..a23621e 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
@@ -21,6 +21,11 @@
import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
+import {
+ Shortcut,
+ ShortcutSection,
+} from '../../../services/shortcuts/shortcuts-config';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -48,6 +53,8 @@
@property({type: Object, notify: true})
change?: ChangeInfo;
+ private readonly shortcuts = appContext.shortcutsService;
+
_computeStarClass(starred?: boolean) {
return starred ? 'active' : '';
}
@@ -83,4 +90,8 @@
})
);
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
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 68df9c1..71f33b7 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
@@ -18,6 +18,7 @@
import '../../../styles/shared-styles';
import '../gr-comment/gr-comment';
import '../../diff/gr-diff/gr-diff';
+import '../gr-copy-clipboard/gr-copy-clipboard';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-comment-thread_html';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -52,7 +53,7 @@
} from '../../../types/common';
import {GrComment} from '../gr-comment/gr-comment';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {LineNumber, FILE} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
import {KnownExperimentId} from '../../../services/flags/flags';
@@ -226,6 +227,8 @@
readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.addEventListener('comment-update', e =>
@@ -495,8 +498,8 @@
return this._orderedComments[this._orderedComments.length - 1] || {};
}
- _handleEKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleEKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 4bd359c..c3faaa5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -116,6 +116,16 @@
top: 4px;
cursor: pointer;
}
+ .fileName gr-copy-clipboard {
+ display: inline-block;
+ visibility: hidden;
+ vertical-align: top;
+ --gr-button-padding: 0px;
+ }
+ .fileName:focus-within gr-copy-clipboard,
+ .fileName:hover gr-copy-clipboard {
+ visibility: visible;
+ }
</style>
<template is="dom-if" if="[[showFilePath]]">
@@ -130,6 +140,10 @@
>
[[_computeDisplayPath(path)]]
</a>
+ <gr-copy-clipboard
+ hideInput=""
+ text="[[_computeDisplayPath(path)]]"
+ ></gr-copy-clipboard>
</template>
</div>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 9dce127..9f65dd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -179,9 +179,6 @@
private async getVisibleEntries(
filter?: (el: Element) => boolean
): Promise<IntersectionObserverEntry[]> {
- if (!this._isIntersectionObserverSupported()) {
- throw new Error('Intersection observing not supported');
- }
if (!this.stops) {
return [];
}
@@ -218,14 +215,6 @@
});
}
- _isIntersectionObserverSupported() {
- // The copy of this method exists in gr-app-element.js under the
- // name _isCursorManagerSupportMoveToVisibleLine
- // If you update this method, you must update gr-app-element.js
- // as well.
- return 'IntersectionObserver' in window;
- }
-
/**
* Set the cursor to an arbitrary stop - if the given element is not one of
* the stops, unset the cursor.
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index 27c6341..74e34a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -21,7 +21,6 @@
import {htmlTemplate} from './gr-dialog_html';
import {customElement, property, observe} from '@polymer/decorators';
import {GrButton} from '../gr-button/gr-button';
-import {KeydownEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
@@ -109,7 +108,7 @@
);
}
- _handleKeydown(e: KeydownEvent) {
+ _handleKeydown(e: KeyboardEvent) {
if (this.confirmOnEnter && e.keyCode === 13) {
this._handleConfirm(e);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index bd1046f..13b195e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -26,12 +26,11 @@
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PaperInputElementExt} from '../../../types/types';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {
AutocompleteQuery,
GrAutocomplete,
} from '../gr-autocomplete/gr-autocomplete';
-import {getKeyboardEvent} from '../../../utils/dom-util';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -205,8 +204,8 @@
this.getGrAutocomplete()) as HTMLInputElement;
}
- _handleEnter(e: CustomKeyboardEvent) {
- e = getKeyboardEvent(e);
+ _handleEnter(event: IronKeyboardEvent) {
+ const e = event.detail.keyboardEvent;
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
@@ -214,8 +213,8 @@
}
}
- _handleEsc(e: CustomKeyboardEvent) {
- e = getKeyboardEvent(e);
+ _handleEsc(event: IronKeyboardEvent) {
+ const e = event.detail.keyboardEvent;
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
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 ce1eec3..434da1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -33,7 +33,7 @@
Item,
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
const MAX_ITEMS_DROPDOWN = 10;
@@ -238,7 +238,7 @@
this._setEmoji(this.$.emojiSuggestions.getCurrentText());
}
- _handleEnterByKey(e: CustomKeyboardEvent) {
+ _handleEnterByKey(e: IronKeyboardEvent) {
// 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) {
@@ -420,7 +420,7 @@
);
}
- private indent(e: CustomKeyboardEvent): void {
+ private indent(e: IronKeyboardEvent): 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 7790c73..7e59692 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,7 @@
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 {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
const basicFixture = fixtureFromElement('gr-textarea');
@@ -240,7 +240,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
@@ -252,7 +252,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.isTrue(indentCommand.notCalled);
@@ -260,7 +260,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.isTrue(indentCommand.notCalled);
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 40434a9..3d1e120 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
@@ -14,104 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/*
-
-How to Add a Keyboard Shortcut
-==============================
-
-A keyboard shortcut is composed of the following parts:
-
- 1. A semantic identifier (e.g. OPEN_CHANGE, NEXT_PAGE)
- 2. Documentation for the keyboard shortcut help dialog
- 3. A binding between key combos and the semantic identifier
- 4. A binding between the semantic identifier and a listener
-
-Parts (1) and (2) for all shortcuts are defined in this file. The semantic
-identifier is declared in the Shortcut enum near the head of this script:
-
- const Shortcut = {
- // ...
- TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE',
- // ...
- };
-
-Immediately following the Shortcut enum definition, there is a _describe
-function defined which is then invoked many times to populate the help dialog.
-Add a new invocation here to document the shortcut:
-
- _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
- 'Hide/show left diff');
-
-When an attached view binds one or more key combos to this shortcut, the help
-dialog will display this text in the given section (in this case, "Diffs"). See
-the ShortcutSection enum immediately below for the list of supported sections.
-
-Part (3), the actual key bindings, are declared by gr-app. In the future, this
-system may be expanded to allow key binding customizations by plugins or user
-preferences. Key bindings are defined in the following forms:
-
- // Ordinary shortcut with a single binding.
- this.bindShortcut(
- Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-
- // Ordinary shortcut with multiple bindings.
- this.bindShortcut(
- Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
-
- // A "go-key" keyboard shortcut, which is combined with a previously and
- // continuously pressed "go" key (the go-key is hard-coded as 'g').
- this.bindShortcut(
- Shortcut.GO_TO_OPENED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'o');
-
- // A "doc-only" keyboard shortcut. This declares the key-binding for help
- // dialog purposes, but doesn't actually implement the binding. It is up
- // to some element to implement this binding using iron-a11y-keys-behavior's
- // keyBindings property.
- this.bindShortcut(
- Shortcut.EXPAND_ALL_COMMENT_THREADS, SPECIAL_SHORTCUT.DOC_ONLY, 'e');
-
-Part (4), the listener definitions, are declared by the view or element that
-implements the shortcut behavior. This is done by implementing a method named
-keyboardShortcuts() in an element that mixes in this behavior, returning an
-object that maps semantic identifiers (as property names) to listener method
-names, like this:
-
- keyboardShortcuts() {
- return {
- [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
- };
- },
-
-You can implement key bindings in an element that is hosted by a view IF that
-element is always attached exactly once under that view (e.g. the search bar in
-gr-app). When that is not the case, you will have to define a doc-only binding
-in gr-app, declare the shortcut in the view that hosts the element, and use
-iron-a11y-keys-behavior's keyBindings attribute to implement the binding in the
-element. An example of this is in comment threads. A diff view supports actions
-on comment threads, but there may be zero or many comment threads attached at
-any given point. So the shortcut is declared as doc-only by the diff view and
-by gr-app, and actually implemented by gr-comment-thread.
-
-NOTE: doc-only shortcuts will not be customizable in the same way that other
-shortcuts are.
-*/
-
import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class';
import {property} from '@polymer/decorators';
import {PolymerElement} from '@polymer/polymer';
import {check, Constructor} from '../../utils/common-util';
-import {getKeyboardEvent, isModifierPressed} from '../../utils/dom-util';
-import {CustomKeyboardEvent} from '../../types/events';
+import {isModifierPressed} from '../../utils/dom-util';
+import {IronKeyboardEvent} from '../../types/events';
import {appContext} from '../../services/app-context';
+import {
+ Shortcut,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+} from '../../services/shortcuts/shortcuts-config';
+import {
+ ShortcutListener,
+ SectionView,
+} from '../../services/shortcuts/shortcuts-service';
-/** Enum for all special shortcuts */
-export enum SPECIAL_SHORTCUT {
- DOC_ONLY = 'DOC_ONLY',
- GO_KEY = 'GO_KEY',
- V_KEY = 'V_KEY',
-}
+export {
+ Shortcut,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+ ShortcutListener,
+ SectionView,
+};
// The maximum age of a keydown event to be used in a jump navigation. This
// is only for cases when the keyup event is lost.
@@ -119,631 +46,6 @@
const V_KEY_TIMEOUT_MS = 1000;
-/**
- * Enum for all shortcut sections, where that shortcut should be applied to.
- */
-export enum ShortcutSection {
- ACTIONS = 'Actions',
- DIFFS = 'Diffs',
- EVERYWHERE = 'Global Shortcuts',
- FILE_LIST = 'File list',
- NAVIGATION = 'Navigation',
- REPLY_DIALOG = 'Reply dialog',
-}
-
-/**
- * Enum for all possible shortcut names.
- */
-export enum Shortcut {
- OPEN_SHORTCUT_HELP_DIALOG = 'OPEN_SHORTCUT_HELP_DIALOG',
- GO_TO_USER_DASHBOARD = 'GO_TO_USER_DASHBOARD',
- GO_TO_OPENED_CHANGES = 'GO_TO_OPENED_CHANGES',
- GO_TO_MERGED_CHANGES = 'GO_TO_MERGED_CHANGES',
- GO_TO_ABANDONED_CHANGES = 'GO_TO_ABANDONED_CHANGES',
- GO_TO_WATCHED_CHANGES = 'GO_TO_WATCHED_CHANGES',
-
- CURSOR_NEXT_CHANGE = 'CURSOR_NEXT_CHANGE',
- CURSOR_PREV_CHANGE = 'CURSOR_PREV_CHANGE',
- OPEN_CHANGE = 'OPEN_CHANGE',
- NEXT_PAGE = 'NEXT_PAGE',
- PREV_PAGE = 'PREV_PAGE',
- TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
- TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
- REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
- OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
- TOGGLE_ATTENTION_SET = 'TOGGLE_ATTENTION_SET',
-
- OPEN_REPLY_DIALOG = 'OPEN_REPLY_DIALOG',
- OPEN_DOWNLOAD_DIALOG = 'OPEN_DOWNLOAD_DIALOG',
- EXPAND_ALL_MESSAGES = 'EXPAND_ALL_MESSAGES',
- COLLAPSE_ALL_MESSAGES = 'COLLAPSE_ALL_MESSAGES',
- UP_TO_DASHBOARD = 'UP_TO_DASHBOARD',
- UP_TO_CHANGE = 'UP_TO_CHANGE',
- TOGGLE_DIFF_MODE = 'TOGGLE_DIFF_MODE',
- REFRESH_CHANGE = 'REFRESH_CHANGE',
- EDIT_TOPIC = 'EDIT_TOPIC',
- DIFF_AGAINST_BASE = 'DIFF_AGAINST_BASE',
- DIFF_AGAINST_LATEST = 'DIFF_AGAINST_LATEST',
- DIFF_BASE_AGAINST_LEFT = 'DIFF_BASE_AGAINST_LEFT',
- DIFF_RIGHT_AGAINST_LATEST = 'DIFF_RIGHT_AGAINST_LATEST',
- DIFF_BASE_AGAINST_LATEST = 'DIFF_BASE_AGAINST_LATEST',
-
- NEXT_LINE = 'NEXT_LINE',
- PREV_LINE = 'PREV_LINE',
- VISIBLE_LINE = 'VISIBLE_LINE',
- NEXT_CHUNK = 'NEXT_CHUNK',
- PREV_CHUNK = 'PREV_CHUNK',
- TOGGLE_ALL_DIFF_CONTEXT = 'TOGGLE_ALL_DIFF_CONTEXT',
- NEXT_COMMENT_THREAD = 'NEXT_COMMENT_THREAD',
- PREV_COMMENT_THREAD = 'PREV_COMMENT_THREAD',
- EXPAND_ALL_COMMENT_THREADS = 'EXPAND_ALL_COMMENT_THREADS',
- COLLAPSE_ALL_COMMENT_THREADS = 'COLLAPSE_ALL_COMMENT_THREADS',
- LEFT_PANE = 'LEFT_PANE',
- RIGHT_PANE = 'RIGHT_PANE',
- TOGGLE_LEFT_PANE = 'TOGGLE_LEFT_PANE',
- NEW_COMMENT = 'NEW_COMMENT',
- SAVE_COMMENT = 'SAVE_COMMENT',
- OPEN_DIFF_PREFS = 'OPEN_DIFF_PREFS',
- TOGGLE_DIFF_REVIEWED = 'TOGGLE_DIFF_REVIEWED',
-
- NEXT_FILE = 'NEXT_FILE',
- PREV_FILE = 'PREV_FILE',
- NEXT_FILE_WITH_COMMENTS = 'NEXT_FILE_WITH_COMMENTS',
- PREV_FILE_WITH_COMMENTS = 'PREV_FILE_WITH_COMMENTS',
- NEXT_UNREVIEWED_FILE = 'NEXT_UNREVIEWED_FILE',
- CURSOR_NEXT_FILE = 'CURSOR_NEXT_FILE',
- CURSOR_PREV_FILE = 'CURSOR_PREV_FILE',
- OPEN_FILE = 'OPEN_FILE',
- TOGGLE_FILE_REVIEWED = 'TOGGLE_FILE_REVIEWED',
- TOGGLE_ALL_INLINE_DIFFS = 'TOGGLE_ALL_INLINE_DIFFS',
- TOGGLE_INLINE_DIFF = 'TOGGLE_INLINE_DIFF',
- TOGGLE_HIDE_ALL_COMMENT_THREADS = 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
- OPEN_FILE_LIST = 'OPEN_FILE_LIST',
-
- OPEN_FIRST_FILE = 'OPEN_FIRST_FILE',
- OPEN_LAST_FILE = 'OPEN_LAST_FILE',
-
- SEARCH = 'SEARCH',
- SEND_REPLY = 'SEND_REPLY',
- EMOJI_DROPDOWN = 'EMOJI_DROPDOWN',
- TOGGLE_BLAME = 'TOGGLE_BLAME',
-}
-
-export type SectionView = Array<{binding: string[][]; text: string}>;
-
-/**
- * The interface for listener for shortcut events.
- */
-export type ShortcutListener = (
- viewMap?: Map<ShortcutSection, SectionView>
-) => void;
-
-interface ShortcutHelpItem {
- shortcut: Shortcut;
- text: string;
-}
-
-// TODO(TS): rename to something more meaningful
-const _help = new Map<ShortcutSection, ShortcutHelpItem[]>();
-
-function _describe(shortcut: Shortcut, section: ShortcutSection, text: string) {
- if (!_help.has(section)) {
- _help.set(section, []);
- }
- const shortcuts = _help.get(section);
- if (shortcuts) {
- shortcuts.push({shortcut, text});
- }
-}
-
-_describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search');
-_describe(
- Shortcut.OPEN_SHORTCUT_HELP_DIALOG,
- ShortcutSection.EVERYWHERE,
- 'Show this dialog'
-);
-_describe(
- Shortcut.GO_TO_USER_DASHBOARD,
- ShortcutSection.EVERYWHERE,
- 'Go to User Dashboard'
-);
-_describe(
- Shortcut.GO_TO_OPENED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Opened Changes'
-);
-_describe(
- Shortcut.GO_TO_MERGED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Merged Changes'
-);
-_describe(
- Shortcut.GO_TO_ABANDONED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Abandoned Changes'
-);
-_describe(
- Shortcut.GO_TO_WATCHED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Watched Changes'
-);
-
-_describe(
- Shortcut.CURSOR_NEXT_CHANGE,
- ShortcutSection.ACTIONS,
- 'Select next change'
-);
-_describe(
- Shortcut.CURSOR_PREV_CHANGE,
- ShortcutSection.ACTIONS,
- 'Select previous change'
-);
-_describe(
- Shortcut.OPEN_CHANGE,
- ShortcutSection.ACTIONS,
- 'Show selected change'
-);
-_describe(Shortcut.NEXT_PAGE, ShortcutSection.ACTIONS, 'Go to next page');
-_describe(Shortcut.PREV_PAGE, ShortcutSection.ACTIONS, 'Go to previous page');
-_describe(
- Shortcut.OPEN_REPLY_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open reply dialog to publish comments and add reviewers'
-);
-_describe(
- Shortcut.OPEN_DOWNLOAD_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open download overlay'
-);
-_describe(
- Shortcut.EXPAND_ALL_MESSAGES,
- ShortcutSection.ACTIONS,
- 'Expand all messages'
-);
-_describe(
- Shortcut.COLLAPSE_ALL_MESSAGES,
- ShortcutSection.ACTIONS,
- 'Collapse all messages'
-);
-_describe(
- Shortcut.REFRESH_CHANGE,
- ShortcutSection.ACTIONS,
- 'Reload the change at the latest patch'
-);
-_describe(
- Shortcut.TOGGLE_CHANGE_REVIEWED,
- ShortcutSection.ACTIONS,
- 'Mark/unmark change as reviewed'
-);
-_describe(
- Shortcut.TOGGLE_FILE_REVIEWED,
- ShortcutSection.ACTIONS,
- 'Toggle review flag on selected file'
-);
-_describe(
- Shortcut.REFRESH_CHANGE_LIST,
- ShortcutSection.ACTIONS,
- 'Refresh list of changes'
-);
-_describe(
- Shortcut.TOGGLE_CHANGE_STAR,
- ShortcutSection.ACTIONS,
- 'Star/unstar change'
-);
-_describe(
- Shortcut.OPEN_SUBMIT_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open submit dialog'
-);
-_describe(
- Shortcut.TOGGLE_ATTENTION_SET,
- ShortcutSection.ACTIONS,
- 'Toggle attention set status'
-);
-_describe(Shortcut.EDIT_TOPIC, ShortcutSection.ACTIONS, 'Add a change topic');
-_describe(
- Shortcut.DIFF_AGAINST_BASE,
- ShortcutSection.ACTIONS,
- 'Diff against base'
-);
-_describe(
- Shortcut.DIFF_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff against latest patchset'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- ShortcutSection.ACTIONS,
- 'Diff base against left'
-);
-_describe(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff right against latest'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff base against latest'
-);
-
-_describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line');
-_describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line');
-_describe(
- Shortcut.DIFF_AGAINST_BASE,
- ShortcutSection.DIFFS,
- 'Diff against base'
-);
-_describe(
- Shortcut.DIFF_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff against latest patchset'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- ShortcutSection.DIFFS,
- 'Diff base against left'
-);
-_describe(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff right against latest'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff base against latest'
-);
-_describe(
- Shortcut.VISIBLE_LINE,
- ShortcutSection.DIFFS,
- 'Move cursor to currently visible code'
-);
-_describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS, 'Go to next diff chunk');
-_describe(
- Shortcut.PREV_CHUNK,
- ShortcutSection.DIFFS,
- 'Go to previous diff chunk'
-);
-_describe(
- Shortcut.TOGGLE_ALL_DIFF_CONTEXT,
- ShortcutSection.DIFFS,
- 'Toggle all diff context'
-);
-_describe(
- Shortcut.NEXT_COMMENT_THREAD,
- ShortcutSection.DIFFS,
- 'Go to next comment thread'
-);
-_describe(
- Shortcut.PREV_COMMENT_THREAD,
- ShortcutSection.DIFFS,
- 'Go to previous comment thread'
-);
-_describe(
- Shortcut.EXPAND_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Expand all comment threads'
-);
-_describe(
- Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Collapse all comment threads'
-);
-_describe(
- Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Hide/Display all comment threads'
-);
-_describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane');
-_describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane');
-_describe(
- Shortcut.TOGGLE_LEFT_PANE,
- ShortcutSection.DIFFS,
- 'Hide/show left diff'
-);
-_describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment');
-_describe(Shortcut.SAVE_COMMENT, ShortcutSection.DIFFS, 'Save comment');
-_describe(
- Shortcut.OPEN_DIFF_PREFS,
- ShortcutSection.DIFFS,
- 'Show diff preferences'
-);
-_describe(
- Shortcut.TOGGLE_DIFF_REVIEWED,
- ShortcutSection.DIFFS,
- 'Mark/unmark file as reviewed'
-);
-_describe(
- Shortcut.TOGGLE_DIFF_MODE,
- ShortcutSection.DIFFS,
- 'Toggle unified/side-by-side diff'
-);
-_describe(
- Shortcut.NEXT_UNREVIEWED_FILE,
- ShortcutSection.DIFFS,
- 'Mark file as reviewed and go to next unreviewed file'
-);
-_describe(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS, 'Toggle blame');
-
-_describe(Shortcut.NEXT_FILE, ShortcutSection.NAVIGATION, 'Go to next file');
-_describe(
- Shortcut.PREV_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to previous file'
-);
-_describe(
- Shortcut.NEXT_FILE_WITH_COMMENTS,
- ShortcutSection.NAVIGATION,
- 'Go to next file that has comments'
-);
-_describe(
- Shortcut.PREV_FILE_WITH_COMMENTS,
- ShortcutSection.NAVIGATION,
- 'Go to previous file that has comments'
-);
-_describe(
- Shortcut.OPEN_FIRST_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to first file'
-);
-_describe(
- Shortcut.OPEN_LAST_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to last file'
-);
-_describe(
- Shortcut.UP_TO_DASHBOARD,
- ShortcutSection.NAVIGATION,
- 'Up to dashboard'
-);
-_describe(Shortcut.UP_TO_CHANGE, ShortcutSection.NAVIGATION, 'Up to change');
-
-_describe(
- Shortcut.CURSOR_NEXT_FILE,
- ShortcutSection.FILE_LIST,
- 'Select next file'
-);
-_describe(
- Shortcut.CURSOR_PREV_FILE,
- ShortcutSection.FILE_LIST,
- 'Select previous file'
-);
-_describe(Shortcut.OPEN_FILE, ShortcutSection.FILE_LIST, 'Go to selected file');
-_describe(
- Shortcut.TOGGLE_ALL_INLINE_DIFFS,
- ShortcutSection.FILE_LIST,
- 'Show/hide all inline diffs'
-);
-_describe(
- Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
- ShortcutSection.FILE_LIST,
- 'Hide/Display all comment threads'
-);
-_describe(
- Shortcut.TOGGLE_INLINE_DIFF,
- ShortcutSection.FILE_LIST,
- 'Show/hide selected inline diff'
-);
-
-_describe(Shortcut.SEND_REPLY, ShortcutSection.REPLY_DIALOG, 'Send reply');
-_describe(
- Shortcut.EMOJI_DROPDOWN,
- ShortcutSection.REPLY_DIALOG,
- 'Emoji dropdown'
-);
-
-/**
- * Shortcut manager, holds all hosts, bindings and listeners.
- */
-export class ShortcutManager {
- private readonly activeHosts = new Map<PolymerElement, Map<string, string>>();
-
- private readonly bindings = new Map<Shortcut, string[]>();
-
- public _testOnly_getBindings() {
- return this.bindings;
- }
-
- public _testOnly_isEmpty() {
- return this.activeHosts.size === 0 && this.listeners.size === 0;
- }
-
- private readonly listeners = new Set<ShortcutListener>();
-
- bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
- this.bindings.set(shortcut, bindings);
- }
-
- getBindingsForShortcut(shortcut: Shortcut) {
- return this.bindings.get(shortcut);
- }
-
- attachHost(host: PolymerElement, shortcuts: Map<string, string>) {
- this.activeHosts.set(host, shortcuts);
- this.notifyListeners();
- }
-
- detachHost(host: PolymerElement) {
- if (this.activeHosts.delete(host)) {
- this.notifyListeners();
- return true;
- }
- return false;
- }
-
- addListener(listener: ShortcutListener) {
- this.listeners.add(listener);
- listener(this.directoryView());
- }
-
- removeListener(listener: ShortcutListener) {
- return this.listeners.delete(listener);
- }
-
- getDescription(section: ShortcutSection, shortcutName: Shortcut) {
- const bindings = _help.get(section);
- let desc = '';
- if (bindings) {
- const binding = bindings.find(
- binding => binding.shortcut === shortcutName
- );
- desc = binding ? binding.text : '';
- }
- return desc;
- }
-
- getShortcut(shortcutName: Shortcut) {
- const bindings = this.bindings.get(shortcutName);
- return bindings
- ? bindings
- .map(binding => this.describeBinding(binding).join('+'))
- .join(',')
- : '';
- }
-
- activeShortcutsBySection() {
- const activeShortcuts = new Set<string>();
- this.activeHosts.forEach(shortcuts => {
- shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut));
- });
-
- const activeShortcutsBySection = new Map<
- ShortcutSection,
- ShortcutHelpItem[]
- >();
- _help.forEach((shortcutList, section) => {
- shortcutList.forEach(shortcutHelp => {
- if (activeShortcuts.has(shortcutHelp.shortcut)) {
- if (!activeShortcutsBySection.has(section)) {
- activeShortcutsBySection.set(section, []);
- }
- // From previous condition, the `get(section)`
- // should always return a valid result
- activeShortcutsBySection.get(section)!.push(shortcutHelp);
- }
- });
- });
- return activeShortcutsBySection;
- }
-
- directoryView() {
- const view = new Map<ShortcutSection, SectionView>();
- this.activeShortcutsBySection().forEach((shortcutHelps, section) => {
- const sectionView: Array<{binding: string[][]; text: string}> = [];
- shortcutHelps.forEach(shortcutHelp => {
- const bindingDesc = this.describeBindings(shortcutHelp.shortcut);
- if (!bindingDesc) {
- return;
- }
- this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => {
- sectionView.push({
- binding: bindingDesc,
- text: shortcutHelp.text,
- });
- });
- });
- view.set(section, sectionView);
- });
- return view;
- }
-
- distributeBindingDesc(bindingDesc: string[][]): string[][][] {
- if (
- bindingDesc.length === 1 ||
- this.comboSetDisplayWidth(bindingDesc) < 21
- ) {
- return [bindingDesc];
- }
- // Find the largest prefix of bindings that is under the
- // size threshold.
- const head = [bindingDesc[0]];
- for (let i = 1; i < bindingDesc.length; i++) {
- head.push(bindingDesc[i]);
- if (this.comboSetDisplayWidth(head) >= 21) {
- head.pop();
- return [head].concat(this.distributeBindingDesc(bindingDesc.slice(i)));
- }
- }
- return [];
- }
-
- comboSetDisplayWidth(bindingDesc: string[][]) {
- const bindingSizer = (binding: string[]) =>
- binding.reduce((acc, key) => acc + key.length, 0);
- // Width is the sum of strings + (n-1) * 2 to account for the word
- // "or" joining them.
- return (
- bindingDesc.reduce((acc, binding) => acc + bindingSizer(binding), 0) +
- 2 * (bindingDesc.length - 1)
- );
- }
-
- describeBindings(shortcut: Shortcut): string[][] | null {
- const bindings = this.bindings.get(shortcut);
- if (!bindings) {
- return null;
- }
- if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
- return bindings
- .slice(1)
- .map(binding => this._describeKey(binding))
- .map(binding => ['g'].concat(binding));
- }
- if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
- return bindings
- .slice(1)
- .map(binding => this._describeKey(binding))
- .map(binding => ['v'].concat(binding));
- }
-
- return bindings
- .filter(binding => binding !== SPECIAL_SHORTCUT.DOC_ONLY)
- .map(binding => this.describeBinding(binding));
- }
-
- _describeKey(key: string) {
- switch (key) {
- case 'shift':
- return 'Shift';
- case 'meta':
- return 'Meta';
- case 'ctrl':
- return 'Ctrl';
- case 'enter':
- return 'Enter';
- case 'up':
- return '\u2191'; // ↑
- case 'down':
- return '\u2193'; // ↓
- case 'left':
- return '\u2190'; // ←
- case 'right':
- return '\u2192'; // →
- default:
- return key;
- }
- }
-
- describeBinding(binding: string) {
- // single key bindings
- if (binding.length === 1) {
- return [binding];
- }
- return binding
- .split(':')[0]
- .split('+')
- .map(part => this._describeKey(part));
- }
-
- notifyListeners() {
- const view = this.directoryView();
- this.listeners.forEach(listener => listener(view));
- }
-}
-
-const shortcutManager = new ShortcutManager();
-
interface IronA11yKeysMixinConstructor {
// Note: this is needed to have same interface as other mixins
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -779,11 +81,7 @@
ShortcutSection = ShortcutSection;
- private _disableKeyboardShortcuts = false;
-
- private readonly restApiService = appContext.restApiService;
-
- private reporting = appContext.reportingService;
+ private readonly shortcuts = appContext.shortcutsService;
/** Used to disable shortcuts when the element is not visible. */
private observer?: IntersectionObserver;
@@ -802,76 +100,18 @@
/** Are shortcuts currently enabled? True only when element is visible. */
private bindingsEnabled = false;
- modifierPressed(event: CustomKeyboardEvent) {
+ modifierPressed(e: IronKeyboardEvent) {
/* We are checking for g/v as modifiers pressed. There are cases such as
* pressing v and then /, where we want the handler for / to be triggered.
* TODO(dhruvsri): find a way to support that keyboard combination
*/
- const e = getKeyboardEvent(event);
return (
isModifierPressed(e) || !!this._inGoKeyMode() || !!this.inVKeyMode()
);
}
- shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent) {
- if (this._disableKeyboardShortcuts) return true;
- const e = getKeyboardEvent(event);
- // TODO(TS): maybe override the EventApi, narrow it down to Element always
- const target = (dom(e) as EventApi).rootTarget as Element;
- const tagName = target.tagName;
- const type = target.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;
- }
- for (let i = 0; e.path && i < e.path.length; i++) {
- // TODO(TS): narrow this down to Element from EventTarget first
- if ((e.path[i] as Element).tagName === 'GR-OVERLAY') {
- return true;
- }
- }
-
- // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
- let key = `${(e as unknown as KeyboardEvent).key}:${e.type}`;
- if (this._inGoKeyMode()) key = 'g+' + key;
- if (this.inVKeyMode()) key = 'v+' + key;
- if (e.shiftKey) key = 'shift+' + key;
- if (e.ctrlKey) key = 'ctrl+' + key;
- if (e.metaKey) key = 'meta+' + key;
- if (e.altKey) key = 'alt+' + key;
- this.reporting.reportInteraction('shortcut-triggered', {
- key,
- from: this.nodeName ?? 'unknown',
- });
- return false;
- }
-
- // Alias for getKeyboardEvent.
- getKeyboardEvent(e: CustomKeyboardEvent) {
- return getKeyboardEvent(e);
- }
-
- bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
- shortcutManager.bindShortcut(shortcut, ...bindings);
- }
-
- createTitle(shortcutName: Shortcut, section: ShortcutSection) {
- const desc = shortcutManager.getDescription(section, shortcutName);
- const shortcut = shortcutManager.getShortcut(shortcutName);
- return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
- }
-
_addOwnKeyBindings(shortcut: Shortcut, handler: string) {
- const bindings = shortcutManager.getBindingsForShortcut(shortcut);
+ const bindings = this.shortcuts.getBindingsForShortcut(shortcut);
if (!bindings) {
return;
}
@@ -896,11 +136,6 @@
override connectedCallback() {
super.connectedCallback();
- this.restApiService.getPreferences().then(prefs => {
- if (prefs?.disable_keyboard_shortcuts) {
- this._disableKeyboardShortcuts = true;
- }
- });
this.createVisibilityObserver();
this.enableBindings();
}
@@ -947,7 +182,7 @@
const shortcuts = new Map<string, string>(
Object.entries(this.keyboardShortcuts())
);
- shortcutManager.attachHost(this, shortcuts);
+ this.shortcuts.attachHost(this, shortcuts);
for (const [key, value] of shortcuts.entries()) {
this._addOwnKeyBindings(key as Shortcut, value);
@@ -983,7 +218,7 @@
private disableBindings() {
if (!this.bindingsEnabled) return;
this.bindingsEnabled = false;
- if (shortcutManager.detachHost(this)) {
+ if (this.shortcuts.detachHost(this)) {
this.removeOwnKeyBindings();
}
}
@@ -996,16 +231,8 @@
return {};
}
- addKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.addListener(listener);
- }
-
- removeKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.removeListener(listener);
- }
-
- _handleVKeyDown(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleVKeyDown(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._shortcut_v_key_last_pressed = Date.now();
}
@@ -1022,11 +249,11 @@
);
}
- _handleVAction(e: CustomKeyboardEvent) {
+ _handleVAction(e: IronKeyboardEvent) {
if (
!this.inVKeyMode() ||
!this._shortcut_v_table.has(e.detail.key) ||
- this.shouldSuppressKeyboardShortcut(e)
+ this.shortcuts.shouldSuppress(e)
) {
return;
}
@@ -1039,8 +266,8 @@
}
}
- _handleGoKeyDown(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleGoKeyDown(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._shortcut_go_key_last_pressed = Date.now();
}
@@ -1059,11 +286,11 @@
);
}
- _handleGoAction(e: CustomKeyboardEvent) {
+ _handleGoAction(e: IronKeyboardEvent) {
if (
!this._inGoKeyMode() ||
!this._shortcut_go_table.has(e.detail.key) ||
- this.shouldSuppressKeyboardShortcut(e)
+ this.shortcuts.shouldSuppress(e)
) {
return;
}
@@ -1077,7 +304,10 @@
}
}
- return Mixin as T & Constructor<KeyboardShortcutMixinInterface>;
+ return Mixin as T &
+ Constructor<
+ KeyboardShortcutMixinInterface & KeyboardShortcutMixinInterfaceTesting
+ >;
};
// The following doesn't work (IronA11yKeysBehavior crashes):
@@ -1090,7 +320,10 @@
// This is a workaround
export const KeyboardShortcutMixin = <T extends Constructor<PolymerElement>>(
superClass: T
-): T & Constructor<KeyboardShortcutMixinInterface> =>
+): T &
+ Constructor<
+ KeyboardShortcutMixinInterface & KeyboardShortcutMixinInterfaceTesting
+ > =>
InternalKeyboardShortcutMixin(
// TODO(TS): mixinBehaviors in some lib is returning: `new () => T` instead
// which will fail the type check due to missing IronA11yKeysBehavior interface
@@ -1101,14 +334,13 @@
/** The interface corresponding to KeyboardShortcutMixin */
export interface KeyboardShortcutMixinInterface {
keyboardShortcuts(): {[key: string]: string | null};
- createTitle(name: Shortcut, section: ShortcutSection): string;
- bindShortcut(shortcut: Shortcut, ...bindings: string[]): void;
- shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent): boolean;
- modifierPressed(event: CustomKeyboardEvent): boolean;
- addKeyboardShortcutDirectoryListener(listener: ShortcutListener): void;
- removeKeyboardShortcutDirectoryListener(listener: ShortcutListener): void;
+ modifierPressed(event: IronKeyboardEvent): boolean;
}
-export function _testOnly_getShortcutManagerInstance() {
- return shortcutManager;
+export interface KeyboardShortcutMixinInterfaceTesting {
+ _shortcut_go_key_last_pressed: number | null;
+ _shortcut_v_key_last_pressed: number | null;
+ _shortcut_go_table: Map<string, string>;
+ _shortcut_v_table: Map<string, string>;
+ _handleGoAction: (e: IronKeyboardEvent) => void;
}
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
deleted file mode 100644
index 4536ecd..0000000
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
+++ /dev/null
@@ -1,424 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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.js';
-import {
- KeyboardShortcutMixin, Shortcut,
- ShortcutManager, ShortcutSection, SPECIAL_SHORTCUT,
-} from './keyboard-shortcut-mixin.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {mockPromise} from '../../test/test-utils.js';
-
-const basicFixture =
- fixtureFromElement('keyboard-shortcut-mixin-test-element');
-
-const withinOverlayFixture = fixtureFromTemplate(html`
-<gr-overlay>
- <keyboard-shortcut-mixin-test-element>
- </keyboard-shortcut-mixin-test-element>
-</gr-overlay>
-`);
-
-class GrKeyboardShortcutMixinTestElement extends
- KeyboardShortcutMixin(PolymerElement) {
- static get is() {
- return 'keyboard-shortcut-mixin-test-element';
- }
-
- get keyBindings() {
- return {
- k: '_handleKey',
- enter: '_handleKey',
- };
- }
-
- _handleKey() {}
-}
-
-customElements.define(GrKeyboardShortcutMixinTestElement.is,
- GrKeyboardShortcutMixinTestElement);
-
-suite('keyboard-shortcut-mixin tests', () => {
- let element;
- let overlay;
-
- setup(() => {
- element = basicFixture.instantiate();
- overlay = withinOverlayFixture.instantiate();
- });
-
- suite('ShortcutManager', () => {
- test('bindings management', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
- assert.deepEqual(
- mgr.getBindingsForShortcut(NEXT_FILE),
- [']', '}', 'right']);
- });
-
- test('getShortcut', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
- assert.equal(mgr.getShortcut(NEXT_FILE), '],},→');
- });
-
- test('getShortcut with modifiers', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, 'Shift+a:key');
- assert.equal(mgr.getShortcut(NEXT_FILE), 'Shift+a');
- });
-
- suite('binding descriptions', () => {
- function mapToObject(m) {
- const o = {};
- m.forEach((v, k) => o[k] = v);
- return o;
- }
-
- test('single combo description', () => {
- const mgr = new ShortcutManager();
- assert.deepEqual(mgr.describeBinding('a'), ['a']);
- assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
- assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
- assert.deepEqual(
- mgr.describeBinding('ctrl+shift+up:keyup'),
- ['Ctrl', 'Shift', '↑']);
- });
-
- test('combo set description', () => {
- const mgr = new ShortcutManager();
- assert.isNull(mgr.describeBindings(Shortcut.NEXT_FILE));
-
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY, 'o');
- assert.deepEqual(
- mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
- [['g', 'o']]);
-
- mgr.bindShortcut(Shortcut.NEXT_FILE, SPECIAL_SHORTCUT.DOC_ONLY,
- ']', 'ctrl+shift+right:keyup');
- assert.deepEqual(
- mgr.describeBindings(Shortcut.NEXT_FILE),
- [[']'], ['Ctrl', 'Shift', '→']]);
-
- mgr.bindShortcut(Shortcut.PREV_FILE, '[');
- assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
- });
-
- test('combo set description width', () => {
- const mgr = new ShortcutManager();
- assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
- assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
- assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
- assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
- assert.strictEqual(
- mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
- 12);
- });
-
- test('distribute shortcut help', () => {
- const mgr = new ShortcutManager();
- assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['g', 'o']]),
- [[['g', 'o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
- [[['ctrl', 'shift', 'meta', 'enter']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'shift', 'meta', 'enter'],
- ['o'],
- ]),
- [
- [['ctrl', 'shift', 'meta', 'enter']],
- [['o']],
- ]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'enter'],
- ['meta', 'enter'],
- ['ctrl', 's'],
- ['meta', 's'],
- ]),
- [
- [['ctrl', 'enter'], ['meta', 'enter']],
- [['ctrl', 's'], ['meta', 's']],
- ]);
- });
-
- test('active shortcuts by section', () => {
- const mgr = new ShortcutManager();
- mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
- mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES, 'g+o');
- mgr.bindShortcut(Shortcut.SEARCH, '/');
-
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {});
-
- mgr.attachHost({}, new Map([[Shortcut.NEXT_FILE, null]]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({}, new Map([[Shortcut.NEXT_LINE, null]]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.DIFFS]: [
- {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
- ],
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({}, new Map([
- [Shortcut.SEARCH, null],
- [Shortcut.GO_TO_OPENED_CHANGES, null],
- ]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.DIFFS]: [
- {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
- ],
- [ShortcutSection.EVERYWHERE]: [
- {shortcut: Shortcut.SEARCH, text: 'Search'},
- {
- shortcut: Shortcut.GO_TO_OPENED_CHANGES,
- text: 'Go to Opened Changes',
- },
- ],
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
- });
-
- test('directory view', () => {
- const mgr = new ShortcutManager();
- mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
- mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY, 'o');
- mgr.bindShortcut(Shortcut.SEARCH, '/');
- mgr.bindShortcut(
- Shortcut.SAVE_COMMENT, 'ctrl+enter', 'meta+enter',
- 'ctrl+s', 'meta+s');
-
- assert.deepEqual(mapToObject(mgr.directoryView()), {});
-
- mgr.attachHost({}, new Map([
- [Shortcut.GO_TO_OPENED_CHANGES, null],
- [Shortcut.NEXT_FILE, null],
- [Shortcut.NEXT_LINE, null],
- [Shortcut.SAVE_COMMENT, null],
- [Shortcut.SEARCH, null],
- ]));
- assert.deepEqual(
- mapToObject(mgr.directoryView()),
- {
- [ShortcutSection.DIFFS]: [
- {binding: [['j']], text: 'Go to next line'},
- {
- binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
- text: 'Save comment',
- },
- {
- binding: [['Ctrl', 's'], ['Meta', 's']],
- text: 'Save comment',
- },
- ],
- [ShortcutSection.EVERYWHERE]: [
- {binding: [['/']], text: 'Search'},
- {binding: [['g', 'o']], text: 'Go to Opened Changes'},
- ],
- [ShortcutSection.NAVIGATION]: [
- {binding: [[']']], text: 'Go to next file'},
- ],
- });
- });
- });
- });
-
- test('doesn’t block kb shortcuts for non-allowed els', async () => {
- const divEl = document.createElement('div');
- element.appendChild(divEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for input els', async () => {
- const inputEl = document.createElement('input');
- element.appendChild(inputEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- await promise;
- });
-
- test('doesn’t block kb shortcuts for checkboxes', async () => {
- const inputEl = document.createElement('input');
- inputEl.setAttribute('type', 'checkbox');
- element.appendChild(inputEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for textarea els', async () => {
- const textareaEl = document.createElement('textarea');
- element.appendChild(textareaEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for anything in a gr-overlay', async () => {
- const divEl = document.createElement('div');
- const element =
- overlay.querySelector('keyboard-shortcut-mixin-test-element');
- element.appendChild(divEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks enter shortcut on an anchor', async () => {
- const anchorEl = document.createElement('a');
- const element =
- overlay.querySelector('keyboard-shortcut-mixin-test-element');
- element.appendChild(anchorEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
- await promise;
- });
-
- test('modifierPressed returns accurate values', () => {
- const spy = sinon.spy(element, 'modifierPressed');
- element._handleKey = e => {
- element.modifierPressed(e);
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- });
-
- suite('GO_KEY timing', () => {
- let handlerStub;
-
- setup(() => {
- element._shortcut_go_table.set('a', '_handleA');
- handlerStub = element._handleA = sinon.stub();
- sinon.stub(Date, 'now').returns(10000);
- });
-
- test('success', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isTrue(handlerStub.calledOnce);
- assert.strictEqual(handlerStub.lastCall.args[0], e);
- });
-
- test('go key not pressed', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = null;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('go key pressed too long ago', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 3000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('should suppress', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('unrecognized key', () => {
- const e = {detail: {key: 'f'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts
new file mode 100644
index 0000000..01ad6cc
--- /dev/null
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright (C) 2016 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 {KeyboardShortcutMixin} from './keyboard-shortcut-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import '../../elements/shared/gr-overlay/gr-overlay';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {IronKeyboardEvent} from '../../types/events';
+
+class GrKeyboardShortcutMixinTestElement extends KeyboardShortcutMixin(
+ PolymerElement
+) {
+ static get is() {
+ return 'keyboard-shortcut-mixin-test-element';
+ }
+
+ get keyBindings() {
+ return {
+ k: '_handleKey',
+ enter: '_handleKey',
+ };
+ }
+
+ _handleKey(_: any) {}
+
+ _handleA(_: any) {}
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'keyboard-shortcut-mixin-test-element': GrKeyboardShortcutMixinTestElement;
+ }
+}
+
+customElements.define(
+ GrKeyboardShortcutMixinTestElement.is,
+ GrKeyboardShortcutMixinTestElement
+);
+
+const basicFixture = fixtureFromElement('keyboard-shortcut-mixin-test-element');
+
+suite('keyboard-shortcut-mixin tests', () => {
+ let element: GrKeyboardShortcutMixinTestElement;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await flush();
+ });
+
+ test('modifierPressed returns accurate values', () => {
+ const spy = sinon.spy(element, 'modifierPressed');
+ element._handleKey = e => {
+ element.modifierPressed(e);
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ });
+
+ suite('GO_KEY timing', () => {
+ let handlerStub: sinon.SinonStub;
+
+ setup(() => {
+ element._shortcut_go_table.set('a', '_handleA');
+ handlerStub = element._handleA = sinon.stub();
+ sinon.stub(Date, 'now').returns(10000);
+ });
+
+ test('success', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isTrue(handlerStub.calledOnce);
+ assert.strictEqual(handlerStub.lastCall.args[0], e);
+ });
+
+ test('go key not pressed', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = null;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('go key pressed too long ago', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 3000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('unrecognized key', () => {
+ const e = {
+ detail: {key: 'f'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index ade9529..3a6f7c5 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -27,6 +27,7 @@
import {ConfigService} from './config/config-service';
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
+import {ShortcutsService} from './shortcuts/shortcuts-service';
type ServiceName = keyof AppContext;
type ServiceCreator<T> = () => T;
@@ -82,5 +83,6 @@
storageService: () => new GrStorageService(),
configService: () => new ConfigService(),
userService: () => new UserService(appContext.restApiService),
+ shortcutsService: () => new ShortcutsService(appContext.reportingService),
});
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 161378d..e5828d6 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -26,6 +26,7 @@
import {ConfigService} from './config/config-service';
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
+import {ShortcutsService} from './shortcuts/shortcuts-service';
export interface AppContext {
flagsService: FlagsService;
@@ -40,6 +41,7 @@
storageService: StorageService;
configService: ConfigService;
userService: UserService;
+ shortcutsService: ShortcutsService;
}
/**
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
new file mode 100644
index 0000000..bd004d7
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -0,0 +1,552 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ */
+
+/** Enum for all special shortcuts */
+export enum SPECIAL_SHORTCUT {
+ DOC_ONLY = 'DOC_ONLY',
+ GO_KEY = 'GO_KEY',
+ V_KEY = 'V_KEY',
+}
+
+/**
+ * Enum for all shortcut sections, where that shortcut should be applied to.
+ */
+export enum ShortcutSection {
+ ACTIONS = 'Actions',
+ DIFFS = 'Diffs',
+ EVERYWHERE = 'Global Shortcuts',
+ FILE_LIST = 'File list',
+ NAVIGATION = 'Navigation',
+ REPLY_DIALOG = 'Reply dialog',
+}
+
+/**
+ * Enum for all possible shortcut names.
+ */
+export enum Shortcut {
+ OPEN_SHORTCUT_HELP_DIALOG = 'OPEN_SHORTCUT_HELP_DIALOG',
+ GO_TO_USER_DASHBOARD = 'GO_TO_USER_DASHBOARD',
+ GO_TO_OPENED_CHANGES = 'GO_TO_OPENED_CHANGES',
+ GO_TO_MERGED_CHANGES = 'GO_TO_MERGED_CHANGES',
+ GO_TO_ABANDONED_CHANGES = 'GO_TO_ABANDONED_CHANGES',
+ GO_TO_WATCHED_CHANGES = 'GO_TO_WATCHED_CHANGES',
+
+ CURSOR_NEXT_CHANGE = 'CURSOR_NEXT_CHANGE',
+ CURSOR_PREV_CHANGE = 'CURSOR_PREV_CHANGE',
+ OPEN_CHANGE = 'OPEN_CHANGE',
+ NEXT_PAGE = 'NEXT_PAGE',
+ PREV_PAGE = 'PREV_PAGE',
+ TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
+ TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
+ REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
+ OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
+ TOGGLE_ATTENTION_SET = 'TOGGLE_ATTENTION_SET',
+
+ OPEN_REPLY_DIALOG = 'OPEN_REPLY_DIALOG',
+ OPEN_DOWNLOAD_DIALOG = 'OPEN_DOWNLOAD_DIALOG',
+ EXPAND_ALL_MESSAGES = 'EXPAND_ALL_MESSAGES',
+ COLLAPSE_ALL_MESSAGES = 'COLLAPSE_ALL_MESSAGES',
+ UP_TO_DASHBOARD = 'UP_TO_DASHBOARD',
+ UP_TO_CHANGE = 'UP_TO_CHANGE',
+ TOGGLE_DIFF_MODE = 'TOGGLE_DIFF_MODE',
+ REFRESH_CHANGE = 'REFRESH_CHANGE',
+ EDIT_TOPIC = 'EDIT_TOPIC',
+ DIFF_AGAINST_BASE = 'DIFF_AGAINST_BASE',
+ DIFF_AGAINST_LATEST = 'DIFF_AGAINST_LATEST',
+ DIFF_BASE_AGAINST_LEFT = 'DIFF_BASE_AGAINST_LEFT',
+ DIFF_RIGHT_AGAINST_LATEST = 'DIFF_RIGHT_AGAINST_LATEST',
+ DIFF_BASE_AGAINST_LATEST = 'DIFF_BASE_AGAINST_LATEST',
+
+ NEXT_LINE = 'NEXT_LINE',
+ PREV_LINE = 'PREV_LINE',
+ VISIBLE_LINE = 'VISIBLE_LINE',
+ NEXT_CHUNK = 'NEXT_CHUNK',
+ PREV_CHUNK = 'PREV_CHUNK',
+ TOGGLE_ALL_DIFF_CONTEXT = 'TOGGLE_ALL_DIFF_CONTEXT',
+ NEXT_COMMENT_THREAD = 'NEXT_COMMENT_THREAD',
+ PREV_COMMENT_THREAD = 'PREV_COMMENT_THREAD',
+ EXPAND_ALL_COMMENT_THREADS = 'EXPAND_ALL_COMMENT_THREADS',
+ COLLAPSE_ALL_COMMENT_THREADS = 'COLLAPSE_ALL_COMMENT_THREADS',
+ LEFT_PANE = 'LEFT_PANE',
+ RIGHT_PANE = 'RIGHT_PANE',
+ TOGGLE_LEFT_PANE = 'TOGGLE_LEFT_PANE',
+ NEW_COMMENT = 'NEW_COMMENT',
+ SAVE_COMMENT = 'SAVE_COMMENT',
+ OPEN_DIFF_PREFS = 'OPEN_DIFF_PREFS',
+ TOGGLE_DIFF_REVIEWED = 'TOGGLE_DIFF_REVIEWED',
+
+ NEXT_FILE = 'NEXT_FILE',
+ PREV_FILE = 'PREV_FILE',
+ NEXT_FILE_WITH_COMMENTS = 'NEXT_FILE_WITH_COMMENTS',
+ PREV_FILE_WITH_COMMENTS = 'PREV_FILE_WITH_COMMENTS',
+ NEXT_UNREVIEWED_FILE = 'NEXT_UNREVIEWED_FILE',
+ CURSOR_NEXT_FILE = 'CURSOR_NEXT_FILE',
+ CURSOR_PREV_FILE = 'CURSOR_PREV_FILE',
+ OPEN_FILE = 'OPEN_FILE',
+ TOGGLE_FILE_REVIEWED = 'TOGGLE_FILE_REVIEWED',
+ TOGGLE_ALL_INLINE_DIFFS = 'TOGGLE_ALL_INLINE_DIFFS',
+ TOGGLE_INLINE_DIFF = 'TOGGLE_INLINE_DIFF',
+ TOGGLE_HIDE_ALL_COMMENT_THREADS = 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
+ OPEN_FILE_LIST = 'OPEN_FILE_LIST',
+
+ OPEN_FIRST_FILE = 'OPEN_FIRST_FILE',
+ OPEN_LAST_FILE = 'OPEN_LAST_FILE',
+
+ SEARCH = 'SEARCH',
+ SEND_REPLY = 'SEND_REPLY',
+ EMOJI_DROPDOWN = 'EMOJI_DROPDOWN',
+ TOGGLE_BLAME = 'TOGGLE_BLAME',
+}
+
+export interface ShortcutHelpItem {
+ shortcut: Shortcut;
+ text: string;
+ bindings: string[];
+}
+
+export const config = new Map<ShortcutSection, ShortcutHelpItem[]>();
+
+function describe(
+ shortcut: Shortcut,
+ section: ShortcutSection,
+ text: string,
+ binding: string,
+ ...moreBindings: string[]
+) {
+ if (!config.has(section)) {
+ config.set(section, []);
+ }
+ const shortcuts = config.get(section);
+ if (shortcuts) {
+ shortcuts.push({shortcut, text, bindings: [binding, ...moreBindings]});
+ }
+}
+
+describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search', '/');
+describe(
+ Shortcut.OPEN_SHORTCUT_HELP_DIALOG,
+ ShortcutSection.EVERYWHERE,
+ 'Show this dialog',
+ '?'
+);
+describe(
+ Shortcut.GO_TO_USER_DASHBOARD,
+ ShortcutSection.EVERYWHERE,
+ 'Go to User Dashboard',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'i'
+);
+describe(
+ Shortcut.GO_TO_OPENED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Opened Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'o'
+);
+describe(
+ Shortcut.GO_TO_MERGED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Merged Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'm'
+);
+describe(
+ Shortcut.GO_TO_ABANDONED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Abandoned Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'a'
+);
+describe(
+ Shortcut.GO_TO_WATCHED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Watched Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'w'
+);
+
+describe(
+ Shortcut.CURSOR_NEXT_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Select next change',
+ 'j'
+);
+describe(
+ Shortcut.CURSOR_PREV_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Select previous change',
+ 'k'
+);
+describe(
+ Shortcut.OPEN_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Show selected change',
+ 'o'
+);
+describe(
+ Shortcut.NEXT_PAGE,
+ ShortcutSection.ACTIONS,
+ 'Go to next page',
+ 'n',
+ ']'
+);
+describe(
+ Shortcut.PREV_PAGE,
+ ShortcutSection.ACTIONS,
+ 'Go to previous page',
+ 'p',
+ '['
+);
+describe(
+ Shortcut.OPEN_REPLY_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open reply dialog to publish comments and add reviewers',
+ 'a:keyup'
+);
+describe(
+ Shortcut.OPEN_DOWNLOAD_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open download overlay',
+ 'd:keyup'
+);
+describe(
+ Shortcut.EXPAND_ALL_MESSAGES,
+ ShortcutSection.ACTIONS,
+ 'Expand all messages',
+ 'x'
+);
+describe(
+ Shortcut.COLLAPSE_ALL_MESSAGES,
+ ShortcutSection.ACTIONS,
+ 'Collapse all messages',
+ 'z'
+);
+describe(
+ Shortcut.REFRESH_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Reload the change at the latest patch',
+ 'shift+r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_CHANGE_REVIEWED,
+ ShortcutSection.ACTIONS,
+ 'Mark/unmark change as reviewed',
+ 'r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_FILE_REVIEWED,
+ ShortcutSection.ACTIONS,
+ 'Toggle review flag on selected file',
+ 'r:keyup'
+);
+describe(
+ Shortcut.REFRESH_CHANGE_LIST,
+ ShortcutSection.ACTIONS,
+ 'Refresh list of changes',
+ 'shift+r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS,
+ 'Star/unstar change',
+ 's:keydown'
+);
+describe(
+ Shortcut.OPEN_SUBMIT_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open submit dialog',
+ 'shift+s'
+);
+describe(
+ Shortcut.TOGGLE_ATTENTION_SET,
+ ShortcutSection.ACTIONS,
+ 'Toggle attention set status',
+ 'shift+t'
+);
+describe(
+ Shortcut.EDIT_TOPIC,
+ ShortcutSection.ACTIONS,
+ 'Add a change topic',
+ 't'
+);
+describe(
+ Shortcut.DIFF_AGAINST_BASE,
+ ShortcutSection.DIFFS,
+ 'Diff against base',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'down',
+ 's'
+);
+describe(
+ Shortcut.DIFF_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff against latest patchset',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'up',
+ 'w'
+);
+describe(
+ Shortcut.DIFF_BASE_AGAINST_LEFT,
+ ShortcutSection.DIFFS,
+ 'Diff base against left',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'left',
+ 'a'
+);
+describe(
+ Shortcut.DIFF_RIGHT_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff right against latest',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'right',
+ 'd'
+);
+describe(
+ Shortcut.DIFF_BASE_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff base against latest',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'b'
+);
+
+describe(
+ Shortcut.NEXT_LINE,
+ ShortcutSection.DIFFS,
+ 'Go to next line',
+ 'j',
+ 'down'
+);
+describe(
+ Shortcut.PREV_LINE,
+ ShortcutSection.DIFFS,
+ 'Go to previous line',
+ 'k',
+ 'up'
+);
+describe(
+ Shortcut.VISIBLE_LINE,
+ ShortcutSection.DIFFS,
+ 'Move cursor to currently visible code',
+ '.'
+);
+describe(
+ Shortcut.NEXT_CHUNK,
+ ShortcutSection.DIFFS,
+ 'Go to next diff chunk',
+ 'n'
+);
+describe(
+ Shortcut.PREV_CHUNK,
+ ShortcutSection.DIFFS,
+ 'Go to previous diff chunk',
+ 'p'
+);
+describe(
+ Shortcut.TOGGLE_ALL_DIFF_CONTEXT,
+ ShortcutSection.DIFFS,
+ 'Toggle all diff context',
+ 'shift+x'
+);
+describe(
+ Shortcut.NEXT_COMMENT_THREAD,
+ ShortcutSection.DIFFS,
+ 'Go to next comment thread',
+ 'shift+n'
+);
+describe(
+ Shortcut.PREV_COMMENT_THREAD,
+ ShortcutSection.DIFFS,
+ 'Go to previous comment thread',
+ 'shift+p'
+);
+describe(
+ Shortcut.EXPAND_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Expand all comment threads',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'e'
+);
+describe(
+ Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Collapse all comment threads',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'shift+e'
+);
+describe(
+ Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Hide/Display all comment threads',
+ 'h'
+);
+describe(
+ Shortcut.LEFT_PANE,
+ ShortcutSection.DIFFS,
+ 'Select left pane',
+ 'shift+left'
+);
+describe(
+ Shortcut.RIGHT_PANE,
+ ShortcutSection.DIFFS,
+ 'Select right pane',
+ 'shift+right'
+);
+describe(
+ Shortcut.TOGGLE_LEFT_PANE,
+ ShortcutSection.DIFFS,
+ 'Hide/show left diff',
+ 'shift+a'
+);
+describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment', 'c');
+describe(
+ Shortcut.SAVE_COMMENT,
+ ShortcutSection.DIFFS,
+ 'Save comment',
+ 'ctrl+enter',
+ 'meta+enter',
+ 'ctrl+s',
+ 'meta+s'
+);
+describe(
+ Shortcut.OPEN_DIFF_PREFS,
+ ShortcutSection.DIFFS,
+ 'Show diff preferences',
+ ','
+);
+describe(
+ Shortcut.TOGGLE_DIFF_REVIEWED,
+ ShortcutSection.DIFFS,
+ 'Mark/unmark file as reviewed',
+ 'r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_DIFF_MODE,
+ ShortcutSection.DIFFS,
+ 'Toggle unified/side-by-side diff',
+ 'm:keyup'
+);
+describe(
+ Shortcut.NEXT_UNREVIEWED_FILE,
+ ShortcutSection.DIFFS,
+ 'Mark file as reviewed and go to next unreviewed file',
+ 'shift+m'
+);
+describe(
+ Shortcut.TOGGLE_BLAME,
+ ShortcutSection.DIFFS,
+ 'Toggle blame',
+ 'b:keyup'
+);
+describe(Shortcut.OPEN_FILE_LIST, ShortcutSection.DIFFS, 'Open file list', 'f');
+describe(
+ Shortcut.NEXT_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to next file',
+ ']'
+);
+describe(
+ Shortcut.PREV_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to previous file',
+ '['
+);
+describe(
+ Shortcut.NEXT_FILE_WITH_COMMENTS,
+ ShortcutSection.NAVIGATION,
+ 'Go to next file that has comments',
+ 'shift+j'
+);
+describe(
+ Shortcut.PREV_FILE_WITH_COMMENTS,
+ ShortcutSection.NAVIGATION,
+ 'Go to previous file that has comments',
+ 'shift+k'
+);
+describe(
+ Shortcut.OPEN_FIRST_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to first file',
+ ']'
+);
+describe(
+ Shortcut.OPEN_LAST_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to last file',
+ '['
+);
+describe(
+ Shortcut.UP_TO_DASHBOARD,
+ ShortcutSection.NAVIGATION,
+ 'Up to dashboard',
+ 'u'
+);
+describe(
+ Shortcut.UP_TO_CHANGE,
+ ShortcutSection.NAVIGATION,
+ 'Up to change',
+ 'u'
+);
+
+describe(
+ Shortcut.CURSOR_NEXT_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Select next file',
+ 'j',
+ 'down'
+);
+describe(
+ Shortcut.CURSOR_PREV_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Select previous file',
+ 'k',
+ 'up'
+);
+describe(
+ Shortcut.OPEN_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Go to selected file',
+ 'o',
+ 'enter'
+);
+describe(
+ Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+ ShortcutSection.FILE_LIST,
+ 'Show/hide all inline diffs',
+ 'shift+i'
+);
+describe(
+ Shortcut.TOGGLE_INLINE_DIFF,
+ ShortcutSection.FILE_LIST,
+ 'Show/hide selected inline diff',
+ 'i'
+);
+
+describe(
+ Shortcut.SEND_REPLY,
+ ShortcutSection.REPLY_DIALOG,
+ 'Send reply',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'ctrl+enter',
+ 'meta+enter'
+);
+describe(
+ Shortcut.EMOJI_DROPDOWN,
+ ShortcutSection.REPLY_DIALOG,
+ 'Emoji dropdown',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ ':'
+);
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
new file mode 100644
index 0000000..d0e2d49
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -0,0 +1,317 @@
+/**
+ * @license
+ * Copyright (C) 2021 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 {
+ config,
+ Shortcut,
+ ShortcutHelpItem,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+} from './shortcuts-config';
+import {disableShortcuts$} from '../user/user-model';
+import {IronKeyboardEvent, isIronKeyboardEvent} from '../../types/events';
+import {isElementTarget} from '../../utils/dom-util';
+import {ReportingService} from '../gr-reporting/gr-reporting';
+
+export type SectionView = Array<{binding: string[][]; text: string}>;
+
+/**
+ * The interface for listener for shortcut events.
+ */
+export type ShortcutListener = (
+ viewMap?: Map<ShortcutSection, SectionView>
+) => void;
+
+const COMBO_KEYS = ['g', 'v'];
+
+/**
+ * Shortcuts service, holds all hosts, bindings and listeners.
+ */
+export class ShortcutsService {
+ /**
+ * Keeps track of the components that are currently active such that we can
+ * show a shortcut help dialog that only shows the shortcuts that are
+ * currently relevant.
+ */
+ private readonly activeHosts = new Map<unknown, Map<string, string>>();
+
+ /** Static map built in the constructor by iterating over the config. */
+ private readonly bindings = new Map<Shortcut, string[]>();
+
+ private readonly listeners = new Set<ShortcutListener>();
+
+ /**
+ * Maps keys (e.g. 'g') to the timestamp when they have last been pressed.
+ * This enabled key combinations like 'g+o' where we can check whether 'g' was
+ * pressed recently when 'o' is processed. Keys of this map must be items of
+ * COMBO_KEYS. Values are Date timestamps in milliseconds.
+ */
+ private readonly keyLastPressed = new Map<string, number>();
+
+ /** Keeps track of the corresponding user preference. */
+ private shortcutsDisabled = false;
+
+ constructor(readonly reporting?: ReportingService) {
+ for (const section of config.keys()) {
+ const items = config.get(section) ?? [];
+ for (const item of items) {
+ this.bindings.set(item.shortcut, item.bindings);
+ }
+ }
+ disableShortcuts$.subscribe(x => (this.shortcutsDisabled = x));
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
+ if (!COMBO_KEYS.includes(e.key)) return;
+ if (this.shouldSuppress(e)) return;
+ this.keyLastPressed.set(e.key, Date.now());
+ });
+ }
+
+ public _testOnly_isEmpty() {
+ return this.activeHosts.size === 0 && this.listeners.size === 0;
+ }
+
+ shouldSuppress(event: IronKeyboardEvent | KeyboardEvent) {
+ if (this.shortcutsDisabled) return true;
+ const e = isIronKeyboardEvent(event) ? event.detail.keyboardEvent : event;
+
+ // 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}`;
+ // TODO(brohlfs): Re-enable reporting of g- and v-keys.
+ // if (this._inGoKeyMode()) key = 'g+' + key;
+ // if (this.inVKeyMode()) key = 'v+' + key;
+ if (e.shiftKey) key = 'shift+' + key;
+ if (e.ctrlKey) key = 'ctrl+' + key;
+ if (e.metaKey) key = 'meta+' + key;
+ if (e.altKey) key = 'alt+' + key;
+ let from = 'unknown';
+ if (isElementTarget(e.currentTarget)) {
+ from = e.currentTarget.tagName;
+ }
+ this.reporting?.reportInteraction('shortcut-triggered', {key, from});
+ return false;
+ }
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ const desc = this.getDescription(section, shortcutName);
+ const shortcut = this.getShortcut(shortcutName);
+ return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
+ }
+
+ getBindingsForShortcut(shortcut: Shortcut) {
+ return this.bindings.get(shortcut);
+ }
+
+ attachHost(host: unknown, shortcuts: Map<string, string>) {
+ this.activeHosts.set(host, shortcuts);
+ this.notifyListeners();
+ }
+
+ detachHost(host: unknown) {
+ if (!this.activeHosts.delete(host)) return false;
+ this.notifyListeners();
+ return true;
+ }
+
+ addListener(listener: ShortcutListener) {
+ this.listeners.add(listener);
+ listener(this.directoryView());
+ }
+
+ removeListener(listener: ShortcutListener) {
+ return this.listeners.delete(listener);
+ }
+
+ getDescription(section: ShortcutSection, shortcutName: Shortcut) {
+ const bindings = config.get(section);
+ if (!bindings) return '';
+ const binding = bindings.find(binding => binding.shortcut === shortcutName);
+ return binding?.text ?? '';
+ }
+
+ getShortcut(shortcutName: Shortcut) {
+ const bindings = this.bindings.get(shortcutName);
+ if (!bindings) return '';
+ return bindings
+ .map(binding => this.describeBinding(binding).join('+'))
+ .join(',');
+ }
+
+ activeShortcutsBySection() {
+ const activeShortcuts = new Set<string>();
+ this.activeHosts.forEach(shortcuts => {
+ shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut));
+ });
+
+ const activeShortcutsBySection = new Map<
+ ShortcutSection,
+ ShortcutHelpItem[]
+ >();
+ config.forEach((shortcutList, section) => {
+ shortcutList.forEach(shortcutHelp => {
+ if (activeShortcuts.has(shortcutHelp.shortcut)) {
+ if (!activeShortcutsBySection.has(section)) {
+ activeShortcutsBySection.set(section, []);
+ }
+ // From previous condition, the `get(section)`
+ // should always return a valid result
+ activeShortcutsBySection.get(section)!.push(shortcutHelp);
+ }
+ });
+ });
+ return activeShortcutsBySection;
+ }
+
+ directoryView() {
+ const view = new Map<ShortcutSection, SectionView>();
+ this.activeShortcutsBySection().forEach((shortcutHelps, section) => {
+ const sectionView: SectionView = [];
+ shortcutHelps.forEach(shortcutHelp => {
+ const bindingDesc = this.describeBindings(shortcutHelp.shortcut);
+ if (!bindingDesc) {
+ return;
+ }
+ this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => {
+ sectionView.push({
+ binding: bindingDesc,
+ text: shortcutHelp.text,
+ });
+ });
+ });
+ view.set(section, sectionView);
+ });
+ return view;
+ }
+
+ distributeBindingDesc(bindingDesc: string[][]): string[][][] {
+ if (
+ bindingDesc.length === 1 ||
+ this.comboSetDisplayWidth(bindingDesc) < 21
+ ) {
+ return [bindingDesc];
+ }
+ // Find the largest prefix of bindings that is under the
+ // size threshold.
+ const head = [bindingDesc[0]];
+ for (let i = 1; i < bindingDesc.length; i++) {
+ head.push(bindingDesc[i]);
+ if (this.comboSetDisplayWidth(head) >= 21) {
+ head.pop();
+ return [head].concat(this.distributeBindingDesc(bindingDesc.slice(i)));
+ }
+ }
+ return [];
+ }
+
+ comboSetDisplayWidth(bindingDesc: string[][]) {
+ const bindingSizer = (binding: string[]) =>
+ binding.reduce((acc, key) => acc + key.length, 0);
+ // Width is the sum of strings + (n-1) * 2 to account for the word
+ // "or" joining them.
+ return (
+ bindingDesc.reduce((acc, binding) => acc + bindingSizer(binding), 0) +
+ 2 * (bindingDesc.length - 1)
+ );
+ }
+
+ describeBindings(shortcut: Shortcut): string[][] | null {
+ const bindings = this.bindings.get(shortcut);
+ if (!bindings) {
+ return null;
+ }
+ if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
+ return bindings
+ .slice(1)
+ .map(binding => this._describeKey(binding))
+ .map(binding => ['g'].concat(binding));
+ }
+ if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
+ return bindings
+ .slice(1)
+ .map(binding => this._describeKey(binding))
+ .map(binding => ['v'].concat(binding));
+ }
+
+ return bindings
+ .filter(binding => binding !== SPECIAL_SHORTCUT.DOC_ONLY)
+ .map(binding => this.describeBinding(binding));
+ }
+
+ _describeKey(key: string) {
+ switch (key) {
+ case 'shift':
+ return 'Shift';
+ case 'meta':
+ return 'Meta';
+ case 'ctrl':
+ return 'Ctrl';
+ case 'enter':
+ return 'Enter';
+ case 'up':
+ return '\u2191'; // ↑
+ case 'down':
+ return '\u2193'; // ↓
+ case 'left':
+ return '\u2190'; // ←
+ case 'right':
+ return '\u2192'; // →
+ default:
+ return key;
+ }
+ }
+
+ describeBinding(binding: string) {
+ // single key bindings
+ if (binding.length === 1) {
+ return [binding];
+ }
+ return binding
+ .split(':')[0]
+ .split('+')
+ .map(part => this._describeKey(part));
+ }
+
+ notifyListeners() {
+ const view = this.directoryView();
+ this.listeners.forEach(listener => listener(view));
+ }
+}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
new file mode 100644
index 0000000..0998a4c
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -0,0 +1,293 @@
+/**
+ * @license
+ * Copyright (C) 2021 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 {ShortcutsService} from '../../services/shortcuts/shortcuts-service';
+import {Shortcut, ShortcutSection} from './shortcuts-config';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+
+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;
+}
+
+suite('shortcuts-service tests', () => {
+ let service: ShortcutsService;
+
+ setup(() => {
+ service = new ShortcutsService();
+ });
+
+ suite('shouldSuppress', () => {
+ test('do not suppress shortcut event from <div>', async () => {
+ await keyEventOn(document.createElement('div'), e => {
+ assert.isFalse(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <input>', async () => {
+ await keyEventOn(document.createElement('input'), e => {
+ assert.isTrue(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <textarea>', async () => {
+ await keyEventOn(document.createElement('textarea'), e => {
+ assert.isTrue(service.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(service.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(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress "enter" shortcut event from <a>', async () => {
+ await keyEventOn(document.createElement('a'), e => {
+ assert.isFalse(service.shouldSuppress(e));
+ });
+ await keyEventOn(
+ document.createElement('a'),
+ e => assert.isTrue(service.shouldSuppress(e)),
+ 13,
+ 'enter'
+ );
+ });
+ });
+
+ test('getShortcut', () => {
+ const NEXT_FILE = Shortcut.NEXT_FILE;
+ assert.equal(service.getShortcut(NEXT_FILE), ']');
+ });
+
+ test('getShortcut with modifiers', () => {
+ const NEXT_FILE = Shortcut.TOGGLE_LEFT_PANE;
+ assert.equal(service.getShortcut(NEXT_FILE), 'Shift+a');
+ });
+
+ suite('binding descriptions', () => {
+ function mapToObject<K, V>(m: Map<K, V>) {
+ const o: any = {};
+ m.forEach((v: V, k: K) => (o[k] = v));
+ return o;
+ }
+
+ test('single combo description', () => {
+ assert.deepEqual(service.describeBinding('a'), ['a']);
+ assert.deepEqual(service.describeBinding('a:keyup'), ['a']);
+ assert.deepEqual(service.describeBinding('ctrl+a'), ['Ctrl', 'a']);
+ assert.deepEqual(service.describeBinding('ctrl+shift+up:keyup'), [
+ 'Ctrl',
+ 'Shift',
+ '↑',
+ ]);
+ });
+
+ test('combo set description', () => {
+ assert.deepEqual(
+ service.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
+ [['g', 'o']]
+ );
+ assert.deepEqual(service.describeBindings(Shortcut.SAVE_COMMENT), [
+ ['Ctrl', 'Enter'],
+ ['Meta', 'Enter'],
+ ['Ctrl', 's'],
+ ['Meta', 's'],
+ ]);
+ assert.deepEqual(service.describeBindings(Shortcut.PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ assert.strictEqual(service.comboSetDisplayWidth([['u']]), 1);
+ assert.strictEqual(service.comboSetDisplayWidth([['g', 'o']]), 2);
+ assert.strictEqual(service.comboSetDisplayWidth([['Shift', 'r']]), 6);
+ assert.strictEqual(service.comboSetDisplayWidth([['x'], ['y']]), 4);
+ assert.strictEqual(
+ service.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
+ 12
+ );
+ });
+
+ test('distribute shortcut help', () => {
+ assert.deepEqual(service.distributeBindingDesc([['o']]), [[['o']]]);
+ assert.deepEqual(service.distributeBindingDesc([['g', 'o']]), [
+ [['g', 'o']],
+ ]);
+ assert.deepEqual(
+ service.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
+ [[['ctrl', 'shift', 'meta', 'enter']]]
+ );
+ assert.deepEqual(
+ service.distributeBindingDesc([
+ ['ctrl', 'shift', 'meta', 'enter'],
+ ['o'],
+ ]),
+ [[['ctrl', 'shift', 'meta', 'enter']], [['o']]]
+ );
+ assert.deepEqual(
+ service.distributeBindingDesc([
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ]),
+ [
+ [
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ],
+ [
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ],
+ ]
+ );
+ });
+
+ test('active shortcuts by section', () => {
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {});
+
+ service.attachHost({}, new Map([[Shortcut.NEXT_FILE, 'null']]));
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+
+ service.attachHost({}, new Map([[Shortcut.NEXT_LINE, 'null']]));
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.DIFFS]: [
+ {
+ shortcut: Shortcut.NEXT_LINE,
+ text: 'Go to next line',
+ bindings: ['j', 'down'],
+ },
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+
+ service.attachHost(
+ {},
+ new Map([
+ [Shortcut.SEARCH, 'null'],
+ [Shortcut.GO_TO_OPENED_CHANGES, 'null'],
+ ])
+ );
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.DIFFS]: [
+ {
+ shortcut: Shortcut.NEXT_LINE,
+ text: 'Go to next line',
+ bindings: ['j', 'down'],
+ },
+ ],
+ [ShortcutSection.EVERYWHERE]: [
+ {
+ shortcut: Shortcut.SEARCH,
+ text: 'Search',
+ bindings: ['/'],
+ },
+ {
+ shortcut: Shortcut.GO_TO_OPENED_CHANGES,
+ text: 'Go to Opened Changes',
+ bindings: ['GO_KEY', 'o'],
+ },
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+ });
+
+ test('directory view', () => {
+ assert.deepEqual(mapToObject(service.directoryView()), {});
+
+ service.attachHost(
+ {},
+ new Map([
+ [Shortcut.GO_TO_OPENED_CHANGES, 'null'],
+ [Shortcut.NEXT_FILE, 'null'],
+ [Shortcut.NEXT_LINE, 'null'],
+ [Shortcut.SAVE_COMMENT, 'null'],
+ [Shortcut.SEARCH, 'null'],
+ ])
+ );
+ assert.deepEqual(mapToObject(service.directoryView()), {
+ [ShortcutSection.DIFFS]: [
+ {binding: [['j'], ['↓']], text: 'Go to next line'},
+ {
+ binding: [
+ ['Ctrl', 'Enter'],
+ ['Meta', 'Enter'],
+ ],
+ text: 'Save comment',
+ },
+ {
+ binding: [
+ ['Ctrl', 's'],
+ ['Meta', 's'],
+ ],
+ text: 'Save comment',
+ },
+ ],
+ [ShortcutSection.EVERYWHERE]: [
+ {binding: [['/']], text: 'Search'},
+ {binding: [['g', 'o']], text: 'Go to Opened Changes'},
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {binding: [[']']], text: 'Go to next file'},
+ ],
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 4115a71..72ce3e1 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -60,3 +60,8 @@
map(preferences => preferences?.my ?? []),
distinctUntilChanged()
);
+
+export const disableShortcuts$ = preferences$.pipe(
+ map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
+ distinctUntilChanged()
+);
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index f1711ac..a0e85b1 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -284,7 +284,7 @@
--font-weight-bold: 500;
--font-weight-h1: 400;
--font-weight-h2: 400;
- --font-weight-h3: 400;
+ --font-weight-h3: var(--font-weight-bold, 500);
--context-control-button-font: var(--font-weight-normal) var(--font-size-normal) var(--font-family);
--code-hint-font-weight: 500;
--image-diff-button-font: var(--font-weight-normal) var(--font-size-normal) var(--font-family);
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 550d3df..949c268 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,9 +30,7 @@
registerTestCleanup,
addIronOverlayBackdropStyleEl,
removeIronOverlayBackdropStyleEl,
- TestKeyboardShortcutBinder,
} from './test-utils';
-import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {safeTypesBridge} from '../utils/safe-types-util';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
import {initGlobalVariables} from '../elements/gr-app-global-var-init';
@@ -45,6 +43,7 @@
import {cleanUpStorage} from '../services/storage/gr-storage_mock';
import {updatePreferences} from '../services/user/user-model';
import {createDefaultPreferences} from '../constants/constants';
+import {appContext} from '../services/app-context';
declare global {
interface Window {
@@ -101,14 +100,13 @@
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(getCleanupsCount(), 0);
+ _testOnlyInitAppContext();
// The following calls is nessecary to avoid influence of previously executed
// tests.
- TestKeyboardShortcutBinder.push();
- _testOnlyInitAppContext();
initGlobalVariables();
_testOnly_initGerritPluginApi();
- const mgr = _testOnly_getShortcutManagerInstance();
- assert.isTrue(mgr._testOnly_isEmpty());
+ const shortcuts = appContext.shortcutsService;
+ assert.isTrue(shortcuts._testOnly_isEmpty());
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
@@ -197,7 +195,6 @@
teardown(() => {
sinon.restore();
cleanupTestUtils();
- TestKeyboardShortcutBinder.pop();
checkGlobalSpace();
removeIronOverlayBackdropStyleEl();
cancelAllTasks();
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index a60c1d1..1cde372 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -17,10 +17,6 @@
import '../types/globals';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
-import {
- _testOnly_getShortcutManagerInstance,
- Shortcut,
-} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {appContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy} from 'sinon';
@@ -53,44 +49,6 @@
return getComputedStyle(el).getPropertyValue('display') !== 'none';
}
-// Some tests/elements can define its own binding. We want to restore bindings
-// at the end of the test. The TestKeyboardShortcutBinder store bindings in
-// stack, so it is possible to override bindings in nested suites.
-export class TestKeyboardShortcutBinder {
- private static stack: TestKeyboardShortcutBinder[] = [];
-
- static push() {
- const testBinder = new TestKeyboardShortcutBinder();
- this.stack.push(testBinder);
- return _testOnly_getShortcutManagerInstance();
- }
-
- static pop() {
- const item = this.stack.pop();
- if (!item) {
- throw new Error('stack is empty');
- }
- item._restoreShortcuts();
- }
-
- private readonly originalBinding: Map<Shortcut, string[]>;
-
- constructor() {
- this.originalBinding = new Map(
- _testOnly_getShortcutManagerInstance()._testOnly_getBindings()
- );
- }
-
- _restoreShortcuts() {
- const bindings =
- _testOnly_getShortcutManagerInstance()._testOnly_getBindings();
- bindings.clear();
- this.originalBinding.forEach((value, key) => {
- bindings.set(key, value);
- });
- }
-}
-
// Provide reset plugins function to clear installed plugins between tests.
// No gr-app found (running tests)
export const resetPlugins = () => {
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 0069321..c78f61a 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PatchSetNum} from './common';
import {UIComment} from '../utils/comment-util';
import {FetchRequest} from './types';
@@ -69,10 +68,6 @@
'editable-content-save': EditableContentSaveEvent;
'location-change': LocationChangeEvent;
'iron-announce': IronAnnounceEvent;
- /* prettier-ignore */
- 'keydown': KeydownEvent;
- /* prettier-ignore */
- 'keypress': KeypressEvent;
'line-mouse-enter': LineNumberEvent;
'line-mouse-leave': LineNumberEvent;
'line-cursor-moved-in': LineNumberEvent;
@@ -148,10 +143,6 @@
}
export type IronAnnounceEvent = CustomEvent<IronAnnounceEventDetail>;
-export type KeydownEvent = CustomKeyboardEvent;
-
-export type KeypressEvent = InputEvent;
-
export interface LocationChangeEventDetail {
hash: string;
pathname: string;
@@ -252,20 +243,28 @@
export type TitleChangeEvent = CustomEvent<TitleChangeEventDetail>;
/**
- * Keyboard events emitted from polymer elements.
+ * Keyboard events emitted from elements using IronA11yKeysBehavior: That means
+ * that the element returns a list of handlers from either `keyBindings()` or
+ * from `keyboardShortcuts()`. This event should not be used in Lit elements
+ * and will be obsolete once the Lit migration is completed.
*/
-export interface CustomKeyboardEvent extends CustomEvent, EventApi {
- event: CustomKeyboardEvent;
- detail: {
- keyboardEvent?: CustomKeyboardEvent;
- // TODO(TS): maybe should mark as optional and check before accessing
- key: string;
- };
- readonly altKey: boolean;
- readonly changedTouches: TouchList;
- readonly ctrlKey: boolean;
- readonly metaKey: boolean;
- readonly shiftKey: boolean;
- readonly keyCode: number;
- readonly repeat: boolean;
+export interface IronKeyboardEvent extends CustomEvent {
+ detail: IronKeyboardEventDetail;
+}
+
+export interface IronKeyboardEventDetail {
+ keyboardEvent: KeyboardEvent;
+ key: string;
+ combo?: string;
+}
+
+export function isIronKeyboardEvent(
+ e: IronKeyboardEvent | Event | CustomEvent
+): e is IronKeyboardEvent {
+ const ike = e as IronKeyboardEvent;
+ return !!ike?.detail?.keyboardEvent;
+}
+
+export interface IronKeyboardEventListener {
+ (evt: IronKeyboardEvent): void;
}
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index c82f5e4..90ee5a5 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -117,9 +117,9 @@
* Ensure only one call is made within THROTTLE_INTERVAL_MS and any call within
* this interval is ignored
*/
-export function throttleWrap(fn: (e: Event) => void) {
+export function throttleWrap<T>(fn: (e: T) => void) {
let lastCall: number | undefined;
- return (e: Event) => {
+ return (e: T) => {
if (
lastCall !== undefined &&
Date.now() - lastCall < THROTTLE_INTERVAL_MS
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 0002254..9e3bc74 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -104,8 +104,11 @@
selector: string
): E | undefined {
if (!el) return undefined;
- const root = el.shadowRoot ?? el;
- return root.querySelector<E>(selector) ?? undefined;
+ if (el.shadowRoot) {
+ const r = el.shadowRoot.querySelector<E>(selector);
+ if (r) return r;
+ }
+ return el.querySelector<E>(selector) ?? undefined;
}
export function queryAndAssert<E extends Element = Element>(
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 7b1f3e3..ead47bb 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -14,10 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {check} from './common-util';
-import {CustomKeyboardEvent} from '../types/events';
+import {IronKeyboardEvent} from '../types/events';
/**
* Event emitted from polymer elements.
@@ -37,6 +36,17 @@
return 'shadowRoot' in el;
}
+export function isElement(node: Node): node is Element {
+ return node.nodeType === 1;
+}
+
+export function isElementTarget(
+ target: EventTarget | null | undefined
+): target is Element {
+ if (!target) return false;
+ return 'nodeType' in target && isElement(target as Node);
+}
+
// TODO: maybe should have a better name for this
function getPathFromNode(el: EventTarget) {
let tagName = '';
@@ -298,23 +308,14 @@
return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
}
-// Deprecated. Try using "normal" KeyboardEvent and modifierPressed() above.
-export function isModifierPressed(event: CustomKeyboardEvent) {
- const e = getKeyboardEvent(event);
- return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
-}
-
-export function isShiftPressed(event: CustomKeyboardEvent) {
- const e = getKeyboardEvent(event);
+export function shiftPressed(e: KeyboardEvent) {
return e.shiftKey;
}
-export function getKeyboardEvent(e: CustomKeyboardEvent): CustomKeyboardEvent {
- const event = dom(e.detail ? e.detail.keyboardEvent : e);
- // TODO(TS): worth checking if this still holds or not, if no, remove this.
- // When e is a keyboardEvent, e.event is not null.
- if ('event' in event && (event as CustomKeyboardEvent).event) {
- return (event as CustomKeyboardEvent).event;
- }
- return event as CustomKeyboardEvent;
+export function isModifierPressed(e: IronKeyboardEvent) {
+ return modifierPressed(e.detail.keyboardEvent);
+}
+
+export function isShiftPressed(e: IronKeyboardEvent) {
+ return shiftPressed(e.detail.keyboardEvent);
}