Merge "Add filepath copy-paste button next to filename in Comments section"
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 7f13731..3cbe546 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_PATTERN;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
@@ -39,6 +40,7 @@
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -169,7 +171,8 @@
Pattern.compile("Assignee changed from: (.*) to: (.*)");
private static final Pattern REMOVED_REVIEWER_PATTERN =
- Pattern.compile("Removed (cc|reviewer) (.*)(\\.| with the following votes)");
+ Pattern.compile(
+ "Removed (cc|reviewer) (.*)(\\.| with the following votes:\n.*)", Pattern.DOTALL);
private static final Pattern REMOVED_VOTE_PATTERN = Pattern.compile("Removed (.*) by (.*)");
@@ -185,12 +188,18 @@
private static final Pattern ON_CODE_OWNER_ADD_REVIEWER_PATTERN =
Pattern.compile("(.*) who was added as reviewer owns the following files");
+
+ private static final String CODE_OWNER_ADD_REVIEWER_TAG =
+ ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addReviewer";
+
private static final String ON_CODE_OWNER_APPROVAL_REGEX = "code-owner approved by (.*):";
private static final String ON_CODE_OWNER_OVERRIDE_REGEX =
"code-owners submit requirement .* overridden by (.*)";
private static final Pattern ON_CODE_OWNER_REVIEW_PATTERN =
Pattern.compile(ON_CODE_OWNER_APPROVAL_REGEX + "|" + ON_CODE_OWNER_OVERRIDE_REGEX);
+ private static final Pattern ON_CODE_OWNER_POST_REVIEW_PATTERN =
+ Pattern.compile("Patch Set [0-9]+:[\\s\\S]*By (voting|removing)[\\s\\S]*");
private static final Pattern REPLY_BY_REASON_PATTERN =
Pattern.compile("(.*) replied on the change");
@@ -583,7 +592,7 @@
}
Matcher matcher = REMOVED_REVIEWER_PATTERN.matcher(originalChangeMessage);
- if (matcher.find() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
+ if (matcher.matches() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
// Since we do not use change messages for reviewer updates on UI, it does not matter what we
// rewrite it to.
return Optional.of(originalChangeMessage.substring(0, matcher.end(1)));
@@ -731,7 +740,11 @@
if (Strings.isNullOrEmpty(originalMessage)) {
return Optional.empty();
}
-
+ Matcher onCodeOwnerPostReviewMatcher =
+ ON_CODE_OWNER_POST_REVIEW_PATTERN.matcher(originalMessage);
+ if (!onCodeOwnerPostReviewMatcher.matches()) {
+ return Optional.empty();
+ }
Matcher onCodeOwnerReviewMatcher = ON_CODE_OWNER_REVIEW_PATTERN.matcher(originalMessage);
while (onCodeOwnerReviewMatcher.find()) {
String accountName =
@@ -823,7 +836,9 @@
for (FooterLine fl : footerLines) {
String footerKey = fl.getKey();
String footerValue = fl.getValue();
- if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
+ if (footerKey.equalsIgnoreCase(FOOTER_TAG.getName())) {
+ fixProgress.tag = footerValue;
+ } else if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
Account.Id oldAssignee = fixProgress.assigneeId;
FixIdentResult fixedAssignee = null;
if (footerValue.equals("")) {
@@ -954,7 +969,8 @@
fixedChangeMessage =
fixCodeOwnersOnReviewChangeMessage(fixProgress.updateAuthorId, originalChangeMessage);
}
- if (!fixedChangeMessage.isPresent()) {
+ if (!fixedChangeMessage.isPresent()
+ && Objects.equals(fixProgress.tag, CODE_OWNER_ADD_REVIEWER_TAG)) {
fixedChangeMessage =
fixCodeOwnersOnAddReviewerChangeMessage(fixProgress, originalChangeMessage);
}
@@ -1194,6 +1210,9 @@
/** {@link RefNames#changeMetaRef} of the change that is being fixed. */
final String changeMetaRef;
+ /** Tag at current commit update. */
+ String tag = null;
+
/** Assignee at current commit update. */
Account.Id assigneeId = null;
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index f316660..19c2bcf 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -34,6 +34,7 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
@@ -1603,19 +1604,20 @@
accountCache.put(duplicateReviewer);
Change c = newChange();
ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
- ChangeUpdate addReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addReviewerUpdate.putReviewer(reviewer.id(), REVIEWER);
addReviewerUpdate.commit();
- ChangeUpdate invalidOnAddReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate invalidOnAddReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
invalidOnAddReviewerUpdate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
+ " * file2.ts\n");
commitsToFix.add(invalidOnAddReviewerUpdate.commit());
- ChangeUpdate addOtherReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addOtherReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addOtherReviewerUpdate.putReviewer(otherUserId, REVIEWER);
addOtherReviewerUpdate.commit();
- ChangeUpdate invalidOnAddReviewerMultipleReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate invalidOnAddReviewerMultipleReviewerUpdate =
+ newCodeOwnerAddReviewerUpdate(c, changeOwner);
invalidOnAddReviewerMultipleReviewerUpdate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
@@ -1624,17 +1626,17 @@
+ "\nMissing Reviewer who was added as reviewer owns the following files:\n"
+ " * file4.java\n");
commitsToFix.add(invalidOnAddReviewerMultipleReviewerUpdate.commit());
- ChangeUpdate addDuplicateReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addDuplicateReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addDuplicateReviewerUpdate.putReviewer(duplicateReviewer.id(), REVIEWER);
addDuplicateReviewerUpdate.commit();
// Reviewer name resolves to multiple accounts in the same change
- ChangeUpdate onAddReviewerUpdateWithDuplicate = newUpdate(c, changeOwner);
+ ChangeUpdate onAddReviewerUpdateWithDuplicate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
onAddReviewerUpdateWithDuplicate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file6.java\n");
commitsToFix.add(onAddReviewerUpdateWithDuplicate.commit());
- ChangeUpdate validOnAddReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate validOnAddReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
validOnAddReviewerUpdate.setChangeMessage(
"Gerrit Account who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
@@ -2249,6 +2251,13 @@
.collect(toImmutableList());
}
+ protected ChangeUpdate newCodeOwnerAddReviewerUpdate(Change c, CurrentUser user)
+ throws Exception {
+ ChangeUpdate update = newUpdate(c, user, true);
+ update.setTag("autogenerated:gerrit:code-owners:addReviewer");
+ return update;
+ }
+
private ImmutableList<String> commitHistoryDiff(BackfillResult result, Change.Id changeId) {
return result.fixedRefDiff.get(RefNames.changeMetaRef(changeId)).stream()
.map(CommitDiff::diff)
diff --git a/package.json b/package.json
index a47ba9f..ebfadc8 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
"test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --browsers ChromeDev --no-single-run --test-files",
"test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --test-files",
+ "test:single:nocompile": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --test-files",
"polylint": "npm run safe_bazelisk test //polygerrit-ui/app:polylint_test",
"polylint:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
},
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..0400030 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';
@@ -563,6 +564,8 @@
private readonly commentsService = appContext.commentsService;
+ private readonly shortcuts = appContext.shortcutsService;
+
private replyDialogResizeObserver?: ResizeObserver;
override keyboardShortcuts() {
@@ -2644,6 +2647,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..74d3cfc 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,6 +82,7 @@
} from '../../../types/common';
import {
pressAndReleaseKeyOn,
+ keyUpOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
@@ -111,25 +107,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
@@ -682,7 +659,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 +692,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 +805,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);
});
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_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index b8c6cde..85e6f25 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);
});
@@ -1574,7 +1548,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 +1575,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 +1590,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 +1637,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 +1651,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 +1665,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 +1678,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');
@@ -1759,13 +1733,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-requirement-hovercard/gr-submit-requirement-hovercard_html.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
index 192a812..b7b4d9c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
@@ -51,13 +51,13 @@
div.section {
margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
display: flex;
+ align-items: center;
}
div.sectionIcon {
flex: 0 0 30px;
}
div.sectionIcon iron-icon {
position: relative;
- top: 2px;
width: 20px;
height: 20px;
}
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 fd85117..f4af06b 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
@@ -60,7 +60,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);
@@ -128,12 +127,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_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..978d98f 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,
@@ -341,6 +342,8 @@
private readonly commentsService = appContext.commentsService;
+ private readonly shortcuts = appContext.shortcutsService;
+
_throttledToggleFileReviewed?: EventListener;
_onRenderHandler?: EventListener;
@@ -1898,6 +1901,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..9aca719 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);
@@ -682,7 +645,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 +671,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 +697,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 +761,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);
});
@@ -1732,7 +1695,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/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index b6fe60b..3bbcd5b 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';
@@ -232,7 +231,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 +305,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;
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-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-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index dba36a4..67f84a5 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -24,23 +24,26 @@
import '../gr-label/gr-label';
import '../gr-tooltip-content/gr-tooltip-content';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-label-info_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {
- ChangeInfo,
AccountInfo,
LabelInfo,
ApprovalInfo,
AccountId,
isQuickLabelInfo,
isDetailedLabelInfo,
+ LabelNameToInfoMap,
} from '../../../types/common';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
import {GrButton} from '../gr-button/gr-button';
import {getVotingRangeOrDefault} from '../../../utils/label-util';
import {appContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {votingStyles} from '../../../styles/gr-voting-styles';
+import {ifDefined} from 'lit/directives/if-defined';
declare global {
interface HTMLElementTagNameMap {
@@ -57,16 +60,12 @@
interface FormattedLabel {
className?: LabelClassName;
- account: ApprovalInfo;
+ account: ApprovalInfo | AccountInfo;
value: string;
}
@customElement('gr-label-info')
-export class GrLabelInfo extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrLabelInfo extends LitElement {
@property({type: Object})
labelInfo?: LabelInfo;
@@ -89,11 +88,148 @@
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ votingStyles,
+ css`
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ .hidden {
+ display: none;
+ }
+ /* Note that most of the .voteChip styles are coming from the
+ gr-voting-styles include. */
+ .voteChip {
+ display: flex;
+ justify-content: center;
+ margin-right: var(--spacing-s);
+ padding: 1px;
+ }
+ .max {
+ background-color: var(--vote-color-approved);
+ }
+ .min {
+ background-color: var(--vote-color-rejected);
+ }
+ .positive {
+ background-color: var(--vote-color-recommended);
+ border-radius: 12px;
+ border: 1px solid var(--vote-outline-recommended);
+ color: var(--chip-color);
+ }
+ .negative {
+ background-color: var(--vote-color-disliked);
+ border-radius: 12px;
+ border: 1px solid var(--vote-outline-disliked);
+ color: var(--chip-color);
+ }
+ .hidden {
+ display: none;
+ }
+ td {
+ vertical-align: top;
+ }
+ tr {
+ min-height: var(--line-height-normal);
+ }
+ gr-tooltip-content {
+ display: block;
+ }
+ gr-button {
+ vertical-align: top;
+ }
+ gr-button::part(paper-button) {
+ height: var(--line-height-normal);
+ width: var(--line-height-normal);
+ padding: 0;
+ }
+ gr-button[disabled] iron-icon {
+ color: var(--border-color);
+ }
+ gr-account-link {
+ --account-max-length: 100px;
+ margin-right: var(--spacing-xs);
+ }
+ iron-icon {
+ height: calc(var(--line-height-normal) - 2px);
+ width: calc(var(--line-height-normal) - 2px);
+ }
+ .labelValueContainer:not(:first-of-type) td {
+ padding-top: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html` <p
+ class="placeholder ${this.computeShowPlaceholder(
+ this.labelInfo,
+ this.change?.labels
+ )}"
+ >
+ No votes
+ </p>
+ <table>
+ ${this.mapLabelInfo(
+ this.labelInfo,
+ this.account,
+ this.change?.labels
+ ).map(mappedLabel => this.renderLabel(mappedLabel))}
+ </table>`;
+ }
+
+ renderLabel(mappedLabel: FormattedLabel) {
+ const {labelInfo, change} = this;
+ return html` <tr class="labelValueContainer">
+ <td>
+ <gr-tooltip-content
+ has-tooltip
+ title="${this._computeValueTooltip(labelInfo, mappedLabel.value)}"
+ >
+ <gr-label class="${mappedLabel.className} voteChip font-small">
+ ${mappedLabel.value}
+ </gr-label>
+ </gr-tooltip-content>
+ </td>
+ <td>
+ <gr-account-link
+ .account="${mappedLabel.account}"
+ .change="${change}"
+ ></gr-account-link>
+ </td>
+ <td>
+ <gr-tooltip-content has-tooltip title="Remove vote">
+ <gr-button
+ link
+ aria-label="Remove vote"
+ @click="${this.onDeleteVote}"
+ data-account-id="${ifDefined(mappedLabel.account._account_id)}"
+ class="deleteBtn ${this.computeDeleteClass(
+ mappedLabel.account,
+ this.mutable,
+ change
+ )}"
+ >
+ <iron-icon icon="gr-icons:delete"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ </td>
+ </tr>`;
+ }
+
/**
* This method also listens on change.labels.*,
* to trigger computation when a label is removed from the change.
*/
- _mapLabelInfo(labelInfo?: LabelInfo, account?: AccountInfo) {
+ private mapLabelInfo(
+ labelInfo?: LabelInfo,
+ account?: AccountInfo,
+ _?: LabelNameToInfoMap
+ ): FormattedLabel[] {
const result: FormattedLabel[] = [];
if (!labelInfo) {
return result;
@@ -108,7 +244,8 @@
{
value: ok ? '👍️' : '👎️',
className: ok ? LabelClassName.POSITIVE : LabelClassName.NEGATIVE,
- account: ok ? labelInfo.approved : labelInfo.rejected,
+ // executed only if approved or rejected is not undefined
+ account: ok ? labelInfo.approved! : labelInfo.rejected!,
},
];
}
@@ -143,7 +280,7 @@
labelClassName = LabelClassName.NEGATIVE;
}
}
- const formattedLabel = {
+ const formattedLabel: FormattedLabel = {
value: `${labelValPrefix}${label.value}`,
className: labelClassName,
account: label,
@@ -167,10 +304,10 @@
* @param reviewer An object describing the reviewer that left the
* vote.
*/
- _computeDeleteClass(
+ private computeDeleteClass(
reviewer: ApprovalInfo,
mutable: boolean,
- change: ChangeInfo
+ change?: ParsedChangeInfo
) {
if (!mutable || !change || !change.removable_reviewers) {
return 'hidden';
@@ -186,7 +323,7 @@
* Closure annotation for Polymer.prototype.splice is off.
* For now, suppressing annotations.
*/
- _onDeleteVote(e: MouseEvent) {
+ private onDeleteVote(e: MouseEvent) {
if (!this.change) return;
e.preventDefault();
@@ -220,7 +357,7 @@
});
}
- _computeValueTooltip(labelInfo: LabelInfo, score: string) {
+ _computeValueTooltip(labelInfo: LabelInfo | undefined, score: string) {
if (
!labelInfo ||
!isDetailedLabelInfo(labelInfo) ||
@@ -235,7 +372,10 @@
* This method also listens change.labels.* in
* order to trigger computation when a label is removed from the change.
*/
- _computeShowPlaceholder(labelInfo?: LabelInfo) {
+ private computeShowPlaceholder(
+ labelInfo?: LabelInfo,
+ _?: LabelNameToInfoMap
+ ) {
if (!labelInfo) {
return '';
}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
deleted file mode 100644
index 1186ee1..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {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="gr-voting-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .placeholder {
- color: var(--deemphasized-text-color);
- }
- .hidden {
- display: none;
- }
- /* Note that most of the .voteChip styles are coming from the
- gr-voting-styles include. */
- .voteChip {
- display: flex;
- justify-content: center;
- margin-right: var(--spacing-s);
- padding: 1px;
- }
- .max {
- background-color: var(--vote-color-approved);
- }
- .min {
- background-color: var(--vote-color-rejected);
- }
- .positive {
- background-color: var(--vote-color-recommended);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-recommended);
- color: var(--chip-color);
- }
- .negative {
- background-color: var(--vote-color-disliked);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-disliked);
- color: var(--chip-color);
- }
- .hidden {
- display: none;
- }
- td {
- vertical-align: top;
- }
- tr {
- min-height: var(--line-height-normal);
- }
- gr-tooltip-content {
- display: block;
- }
- gr-button {
- display: block;
- vertical-align: top;
- --gr-button-padding: 1px;
- }
- gr-button[disabled] iron-icon {
- color: var(--border-color);
- }
- gr-account-link {
- --account-max-length: 100px;
- margin-right: var(--spacing-xs);
- }
- iron-icon {
- height: calc(var(--line-height-normal) - 2px);
- width: calc(var(--line-height-normal) - 2px);
- }
- .labelValueContainer:not(:first-of-type) td {
- padding-top: var(--spacing-s);
- }
- </style>
- <p
- class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]"
- >
- No votes
- </p>
- <table>
- <template
- is="dom-repeat"
- items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]"
- as="mappedLabel"
- >
- <tr class="labelValueContainer">
- <td>
- <gr-tooltip-content
- has-tooltip
- title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
- >
- <gr-label class$="[[mappedLabel.className]] voteChip font-small">
- [[mappedLabel.value]]
- </gr-label>
- </gr-tooltip-content>
- </td>
- <td>
- <gr-account-link
- account="[[mappedLabel.account]]"
- change="[[change]]"
- ></gr-account-link>
- </td>
- <td>
- <gr-tooltip-content has-tooltip title="Remove vote">
- <gr-button
- link=""
- aria-label="Remove vote"
- on-click="_onDeleteVote"
- data-account-id$="[[mappedLabel.account._account_id]]"
- class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
- >
- <iron-icon icon="gr-icons:delete"></iron-icon>
- </gr-button>
- </gr-tooltip-content>
- </td>
- </tr>
- </template>
- </table>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index b1bd6fa..cad1f69 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -66,15 +66,17 @@
element.labelInfo = label;
element.label = 'Code-Review';
- await flush();
+ await element.updateComplete;
});
- test('_computeCanDeleteVote', () => {
+ test('_computeCanDeleteVote', async () => {
element.mutable = false;
+ await element.updateComplete;
const removeButton = queryAndAssert<GrButton>(element, 'gr-button');
assert.isTrue(isHidden(removeButton));
element.change!.removable_reviewers = [account];
element.mutable = true;
+ await element.updateComplete;
assert.isFalse(isHidden(removeButton));
});
@@ -109,14 +111,14 @@
suite('label color and order', () => {
test('valueless label rejected', async () => {
element.labelInfo = {rejected: {name: 'someone'}};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('negative'));
});
test('valueless label approved', async () => {
element.labelInfo = {approved: {name: 'someone'}};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('positive'));
});
@@ -137,7 +139,7 @@
'+2': 'Ready to submit',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('positive'));
@@ -157,7 +159,7 @@
'+1': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('min'));
@@ -175,7 +177,7 @@
'+2': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('positive'));
@@ -195,7 +197,7 @@
'+1': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const chips = queryAll<GrAccountLink>(element, 'gr-account-link');
assert.equal(chips[0].account!._account_id, element.account._account_id);
});
@@ -217,7 +219,7 @@
assert.equal(element._computeValueTooltip(labelInfo, score), '');
});
- test('placeholder', () => {
+ test('placeholder', async () => {
const values = {
'0': 'No score',
'+1': 'good',
@@ -226,30 +228,37 @@
'-2': 'terrible',
};
element.labelInfo = {};
+ await element.updateComplete;
assert.isFalse(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {all: [], values};
+ await element.updateComplete;
assert.isFalse(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {rejected: account};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {rejected: account, all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {approved: account};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {approved: account, all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
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..2f249e7 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,88 +14,6 @@
* 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';
@@ -105,13 +23,23 @@
import {getKeyboardEvent, isModifierPressed} from '../../utils/dom-util';
import {CustomKeyboardEvent} 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 +47,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
@@ -783,7 +86,9 @@
private readonly restApiService = appContext.restApiService;
- private reporting = appContext.reportingService;
+ private readonly reporting = appContext.reportingService;
+
+ private readonly shortcuts = appContext.shortcutsService;
/** Used to disable shortcuts when the element is not visible. */
private observer?: IntersectionObserver;
@@ -860,18 +165,8 @@
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;
}
@@ -947,7 +242,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 +278,7 @@
private disableBindings() {
if (!this.bindingsEnabled) return;
this.bindingsEnabled = false;
- if (shortcutManager.detachHost(this)) {
+ if (this.shortcuts.detachHost(this)) {
this.removeOwnKeyBindings();
}
}
@@ -996,14 +291,6 @@
return {};
}
- addKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.addListener(listener);
- }
-
- removeKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.removeListener(listener);
- }
-
_handleVKeyDown(e: CustomKeyboardEvent) {
if (this.shouldSuppressKeyboardShortcut(e)) return;
this._shortcut_v_key_last_pressed = Date.now();
@@ -1077,7 +364,10 @@
}
}
- return Mixin as T & Constructor<KeyboardShortcutMixinInterface>;
+ return Mixin as T &
+ Constructor<
+ KeyboardShortcutMixinInterface & KeyboardShortcutMixinInterfaceTesting
+ >;
};
// The following doesn't work (IronA11yKeysBehavior crashes):
@@ -1090,7 +380,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 +394,14 @@
/** 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;
}
-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: CustomKeyboardEvent) => 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..6350bf9
--- /dev/null
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts
@@ -0,0 +1,243 @@
+/**
+ * @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 {html} from '@polymer/polymer/lib/utils/html-tag';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {mockPromise, queryAndAssert} from '../../test/test-utils';
+import '../../elements/shared/gr-overlay/gr-overlay';
+import {GrOverlay} from '../../elements/shared/gr-overlay/gr-overlay';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {CustomKeyboardEvent} 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');
+
+const withinOverlayFixture = fixtureFromTemplate(html`
+ <gr-overlay>
+ <keyboard-shortcut-mixin-test-element>
+ </keyboard-shortcut-mixin-test-element>
+ </gr-overlay>
+`);
+
+suite('keyboard-shortcut-mixin tests', () => {
+ let element: GrKeyboardShortcutMixinTestElement;
+ let overlay: GrOverlay;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ overlay = withinOverlayFixture.instantiate() as GrOverlay;
+ await flush();
+ });
+
+ 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 = queryAndAssert<GrKeyboardShortcutMixinTestElement>(
+ overlay,
+ '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 = queryAndAssert<GrKeyboardShortcutMixinTestElement>(
+ overlay,
+ '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: 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: () => {},
+ } as CustomKeyboardEvent;
+ 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: () => {},
+ } as CustomKeyboardEvent;
+ 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: () => {},
+ } as CustomKeyboardEvent;
+ 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: () => {},
+ } as CustomKeyboardEvent;
+ 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: () => {},
+ } as CustomKeyboardEvent;
+ 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/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index ade9529..597776d 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(),
});
}
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..3cf46bd
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -0,0 +1,240 @@
+/**
+ * @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';
+
+export type SectionView = Array<{binding: string[][]; text: string}>;
+
+/**
+ * The interface for listener for shortcut events.
+ */
+export type ShortcutListener = (
+ viewMap?: Map<ShortcutSection, SectionView>
+) => void;
+
+/**
+ * Shortcuts service, holds all hosts, bindings and listeners.
+ */
+export class ShortcutsService {
+ private readonly activeHosts = new Map<unknown, Map<string, string>>();
+
+ private readonly bindings = new Map<Shortcut, string[]>();
+
+ private readonly listeners = new Set<ShortcutListener>();
+
+ constructor() {
+ for (const section of config.keys()) {
+ const items = config.get(section) ?? [];
+ for (const item of items) {
+ this.bindings.set(item.shortcut, item.bindings);
+ }
+ }
+ }
+
+ public _testOnly_isEmpty() {
+ return this.activeHosts.size === 0 && this.listeners.size === 0;
+ }
+
+ 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..a48fa92
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -0,0 +1,224 @@
+/**
+ * @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';
+
+suite('shortcuts-service tests', () => {
+ test('getShortcut', () => {
+ const mgr = new ShortcutsService();
+ const NEXT_FILE = Shortcut.NEXT_FILE;
+ assert.equal(mgr.getShortcut(NEXT_FILE), ']');
+ });
+
+ test('getShortcut with modifiers', () => {
+ const mgr = new ShortcutsService();
+ const NEXT_FILE = Shortcut.TOGGLE_LEFT_PANE;
+ assert.equal(mgr.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', () => {
+ const mgr = new ShortcutsService();
+ 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 ShortcutsService();
+ assert.deepEqual(mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES), [
+ ['g', 'o'],
+ ]);
+ assert.deepEqual(mgr.describeBindings(Shortcut.SAVE_COMMENT), [
+ ['Ctrl', 'Enter'],
+ ['Meta', 'Enter'],
+ ['Ctrl', 's'],
+ ['Meta', 's'],
+ ]);
+ assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ const mgr = new ShortcutsService();
+ 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 ShortcutsService();
+ 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 ShortcutsService();
+ 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',
+ bindings: [']'],
+ },
+ ],
+ });
+
+ mgr.attachHost({}, new Map([[Shortcut.NEXT_LINE, 'null']]));
+ assert.deepEqual(mapToObject(mgr.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: [']'],
+ },
+ ],
+ });
+
+ 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',
+ 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', () => {
+ const mgr = new ShortcutsService();
+
+ 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'},
+ ],
+ });
+ });
+ });
+});
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/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>(