summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Rohlfs <brohlfs@google.com>2021-10-21 14:29:01 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2021-10-21 14:29:01 +0000
commit5b8cc9e26c134bde649e4c168c360b917df49e22 (patch)
treefa19525efbb41dd608419cfa5927477c35e31e5a
parent242b1330bea7f70faf55dd0673760261c8935613 (diff)
parent771d18a0162fd37f3fae010eeb7a569e57cfb271 (diff)
Merge changes I09997238,I55a954ba,Id5e1ece4,I83028eff
* changes: Replace KeyboardShortcutMixin by addShortcut() util in 5 components Replace KeyboardShortcutMixin by addShortcut() util in 4 components Replace KeyboardShortcutMixin by addShortcut() util in 3 dialogs Add precise modifier matching to the new shortcut handler
-rw-r--r--polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts36
-rw-r--r--polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts32
-rw-r--r--polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts35
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts6
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts20
-rw-r--r--polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts6
-rw-r--r--polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts36
-rw-r--r--polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts6
-rw-r--r--polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts6
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts18
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js2
-rw-r--r--polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts29
-rw-r--r--polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts39
-rw-r--r--polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts14
-rw-r--r--polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts6
-rw-r--r--polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts8
-rw-r--r--polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts27
-rw-r--r--polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts25
-rw-r--r--polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts46
-rw-r--r--polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts12
-rw-r--r--polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts36
-rw-r--r--polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js6
-rw-r--r--polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts55
-rw-r--r--polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts81
-rw-r--r--polygerrit-ui/app/test/common-test-setup-karma.ts2
-rw-r--r--polygerrit-ui/app/utils/dom-util.ts75
-rw-r--r--polygerrit-ui/app/utils/dom-util_test.js155
-rw-r--r--polygerrit-ui/app/utils/dom-util_test.ts269
28 files changed, 665 insertions, 423 deletions
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
index 07da53f708..df537e0fad 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
@@ -19,9 +19,9 @@ import '../../shared/gr-dialog/gr-dialog';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-confirm-abandon-dialog_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {customElement, property} from '@polymer/decorators';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
export interface GrConfirmAbandonDialog {
$: {
@@ -35,11 +35,8 @@ declare global {
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-confirm-abandon-dialog')
-export class GrConfirmAbandonDialog extends base {
+export class GrConfirmAbandonDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -59,18 +56,31 @@ export class GrConfirmAbandonDialog extends base {
@property({type: String})
message = '';
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
}
- resetFocus() {
- this.$.messageInput.textarea.focus();
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, _ =>
+ this._confirm()
+ )
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, _ =>
+ this._confirm()
+ )
+ );
}
- _handleEnterKey() {
- this._confirm();
+ resetFocus() {
+ this.$.messageInput.textarea.focus();
}
_handleConfirmTap(e: Event) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index 8e6521d75e..b3bbc8a610 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -19,17 +19,14 @@ import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-dialog/gr-dialog';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-confirm-move-dialog_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {customElement, property} from '@polymer/decorators';
import {BranchName, RepoName} from '../../../types/common';
import {appContext} from '../../../services/app-context';
import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
const SUGGESTIONS_LIMIT = 15;
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
// This is used to make sure 'branch'
// can be typed as BranchName.
export interface GrConfirmMoveDialog {
@@ -39,7 +36,7 @@ export interface GrConfirmMoveDialog {
}
@customElement('gr-confirm-move-dialog')
-export class GrConfirmMoveDialog extends base {
+export class GrConfirmMoveDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -68,10 +65,27 @@ export class GrConfirmMoveDialog extends base {
@property({type: Object})
_query?: (input: string) => Promise<{name: BranchName}[]>;
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleConfirmTap',
- };
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, e =>
+ this._handleConfirmTap(e)
+ )
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, e =>
+ this._handleConfirmTap(e)
+ )
+ );
}
private readonly restApiService = appContext.restApiService;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index d0865c9d4b..92f4a873c2 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -31,8 +31,8 @@ import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-
import {GrButton} from '../../shared/gr-button/gr-button';
import {hasOwnProperty} from '../../../utils/common-util';
import {GrOverlayStops} from '../../shared/gr-overlay/gr-overlay';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {addShortcut} from '../../../utils/dom-util';
export interface GrDownloadDialog {
$: {
@@ -42,11 +42,8 @@ export interface GrDownloadDialog {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-download-dialog')
-export class GrDownloadDialog extends base {
+export class GrDownloadDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -69,14 +66,22 @@ export class GrDownloadDialog extends base {
@property({type: String})
_selectedScheme?: string;
- get keyBindings() {
- return {
- 1: '_handleNumberKey',
- 2: '_handleNumberKey',
- 3: '_handleNumberKey',
- 4: '_handleNumberKey',
- 5: '_handleNumberKey',
- };
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ for (const key of ['1', '2', '3', '4', '5']) {
+ this.cleanups.push(
+ addShortcut(this, {key}, e => this._handleNumberKey(e))
+ );
+ }
}
@computed('change', 'patchNum')
@@ -98,8 +103,8 @@ export class GrDownloadDialog extends base {
return [];
}
- _handleNumberKey(e: CustomEvent) {
- const index = Number(e.detail.key) - 1;
+ _handleNumberKey(e: KeyboardEvent) {
+ const index = Number(e.key) - 1;
const commands = this._computeDownloadCommands(
this.change,
this.patchNum,
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 50bb665191..ae3eee570f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -45,7 +45,6 @@ import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
import {
- KeyboardShortcutMixin,
Shortcut,
ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -65,11 +64,8 @@ export interface GrFileListHeader {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-file-list-header')
-export class GrFileListHeader extends base {
+export class GrFileListHeader extends PolymerElement {
static get template() {
return htmlTemplate;
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 74c079b639..4fd5ff32b7 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -49,8 +49,10 @@ import {
SpecialFilePath,
} from '../../../constants/constants';
import {
+ addGlobalShortcut,
descendedFromClass,
isShiftPressed,
+ Key,
modifierPressed,
toggleClass,
} from '../../../utils/dom-util';
@@ -319,11 +321,8 @@ export class GrFileList extends base {
disconnected$ = new Subject();
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
override keyboardShortcuts() {
return {
@@ -415,6 +414,9 @@ export class GrFileList extends base {
this.reporting.error(new Error('dynamic header/content mismatch'));
}
});
+ this.cleanups.push(
+ addGlobalShortcut({key: Key.ESC}, e => this._handleEscKey(e))
+ );
}
override disconnectedCallback() {
@@ -423,6 +425,8 @@ export class GrFileList extends base {
this.fileCursor.unsetCursor();
this._cancelDiffs();
this.loadingTask?.cancel();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
@@ -1542,10 +1546,8 @@ export class GrFileList extends base {
return undefined;
}
- _handleEscKey(e: IronKeyboardEvent) {
- if (this.shortcuts.shouldSuppress(e) || this.shortcuts.modifierPressed(e)) {
- return;
- }
+ _handleEscKey(e: KeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this._displayLine = false;
}
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index cd79bbadaa..e16c073a55 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -22,7 +22,6 @@ import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-messages-list_html';
import {
- KeyboardShortcutMixin,
Shortcut,
ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -201,11 +200,8 @@ export interface GrMessagesList {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-messages-list')
-export class GrMessagesList extends base {
+export class GrMessagesList extends PolymerElement {
static get template() {
return htmlTemplate;
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 10e04f776d..b8932e53e0 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -38,7 +38,6 @@ import {
ReviewerState,
SpecialFilePath,
} from '../../../constants/constants';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {
accountOrGroupKey,
isReviewerOrCC,
@@ -116,6 +115,7 @@ import {debounce, DelayedTask} from '../../../utils/async-util';
import {StorageLocation} from '../../../services/storage/gr-storage';
import {Interaction, Timing} from '../../../constants/reporting';
import {getReplyByReason} from '../../../utils/attention-set-util';
+import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -163,11 +163,8 @@ export interface GrReplyDialog {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-reply-dialog')
-export class GrReplyDialog extends base {
+export class GrReplyDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -368,12 +365,8 @@ export class GrReplyDialog extends base {
private storeTask?: DelayedTask;
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
constructor() {
super();
@@ -391,6 +384,17 @@ export class GrReplyDialog extends base {
if (account) this._account = account;
});
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, _ =>
+ this._submit()
+ )
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, _ =>
+ this._submit()
+ )
+ );
+ this.cleanups.push(addShortcut(this, {key: Key.ESC}, _ => this.cancel()));
this.addEventListener('comment-editing-changed', e => {
this._commentEditing = (e as CustomEvent).detail;
});
@@ -418,6 +422,8 @@ export class GrReplyDialog extends base {
override disconnectedCallback() {
this.storeTask?.cancel();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
@@ -492,14 +498,6 @@ export class GrReplyDialog extends base {
return (selectorEl as GrLabelScoreRow).selectedValue;
}
- _handleEscKey() {
- this.cancel();
- }
-
- _handleEnterKey() {
- this._submit();
- }
-
@observe('_ccs.splices')
_ccsChanged(splices: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>) {
this._reviewerTypeChanged(splices, ReviewerType.CC);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index c4bff571b3..5a11f470a3 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -1799,7 +1799,7 @@ suite('gr-reply-dialog tests', () => {
test('emits cancel on esc key', () => {
const cancelHandler = sinon.spy();
element.addEventListener('cancel', cancelHandler);
- pressAndReleaseKeyOn(element, 27, null, 'esc');
+ pressAndReleaseKeyOn(element, 27, null, 'Escape');
flush();
assert.isTrue(cancelHandler.called);
@@ -1808,14 +1808,14 @@ suite('gr-reply-dialog tests', () => {
test('should not send on enter key', () => {
stubSaveReview(() => undefined);
element.addEventListener('send', () => assert.fail('wrongly called'));
- pressAndReleaseKeyOn(element, 13, null, 'enter');
+ pressAndReleaseKeyOn(element, 13, null, 'Enter');
});
test('emit send on ctrl+enter key', async () => {
stubSaveReview(() => undefined);
const promise = mockPromise();
element.addEventListener('send', () => promise.resolve());
- pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
+ pressAndReleaseKeyOn(element, 13, 'ctrl', 'Enter');
await promise;
});
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 541d8776b8..c91ae5a0a5 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -21,7 +21,6 @@ import '../../../styles/gr-font-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html';
import {
- KeyboardShortcutMixin,
ShortcutSection,
ShortcutListener,
SectionView,
@@ -40,11 +39,8 @@ interface SectionShortcut {
shortcuts?: SectionView;
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-keyboard-shortcuts-dialog')
-export class GrKeyboardShortcutsDialog extends base {
+export class GrKeyboardShortcutsDialog extends PolymerElement {
static get template() {
return htmlTemplate;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 084f9f6eb0..236f00f0fb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -107,7 +107,7 @@ import {
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
-import {toggleClass} from '../../../utils/dom-util';
+import {addGlobalShortcut, Key, toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
import {throttleWrap} from '../../../utils/async-util';
import {changeComments$} from '../../../services/comments/comments-model';
@@ -281,11 +281,8 @@ export class GrDiffView extends base {
patchNum?: PatchSetNum;
} = {};
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
override keyboardShortcuts() {
return {
@@ -373,6 +370,9 @@ export class GrDiffView extends base {
this.cursor.reInitCursor();
};
this.$.diffHost.addEventListener('render', this._onRenderHandler);
+ this.cleanups.push(
+ addGlobalShortcut({key: Key.ESC}, e => this._handleEscKey(e))
+ );
}
override disconnectedCallback() {
@@ -381,6 +381,8 @@ export class GrDiffView extends base {
if (this._onRenderHandler) {
this.$.diffHost.removeEventListener('render', this._onRenderHandler);
}
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
@@ -531,10 +533,8 @@ export class GrDiffView extends base {
this._setReviewed(!this.$.reviewed.checked);
}
- _handleEscKey(e: IronKeyboardEvent) {
+ _handleEscKey(e: KeyboardEvent) {
if (this.shortcuts.shouldSuppress(e)) return;
- if (this.shortcuts.modifierPressed(e)) return;
-
e.preventDefault();
this.$.diffHost.displayLine = false;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index cc35c3c807..d45ca0e177 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -512,7 +512,7 @@ suite('gr-diff-view tests', () => {
assert(computeContainerClassStub.lastCall.calledWithExactly(
false, 'SIDE_BY_SIDE', true));
- MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
assert(computeContainerClassStub.lastCall.calledWithExactly(
false, 'SIDE_BY_SIDE', false));
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index ad7e015a37..24ebd675b8 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -22,7 +22,6 @@ import '../gr-default-editor/gr-default-editor';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-editor-view_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {
GerritNav,
GenerateUrlEditViewParameters,
@@ -47,7 +46,7 @@ import {changeIsMerged, changeIsAbandoned} from '../../../utils/change-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDefaultEditor} from '../gr-default-editor/gr-default-editor';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {IronKeyboardEvent} from '../../../types/events';
+import {addShortcut, Modifier} from '../../../utils/dom-util';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -69,11 +68,8 @@ export interface GrEditorView {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-editor-view')
-export class GrEditorView extends base {
+export class GrEditorView extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -141,11 +137,8 @@ export class GrEditorView extends base {
// Tests use this so needs to be non private
storeTask?: DelayedTask;
- get keyBindings() {
- return {
- 'ctrl+s meta+s': '_handleSaveShortcut',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
constructor() {
super();
@@ -159,10 +152,22 @@ export class GrEditorView extends base {
this._getEditPrefs().then(prefs => {
this._prefs = prefs;
});
+ this.cleanups.push(
+ addShortcut(this, {key: 's', modifiers: [Modifier.CTRL_KEY]}, e =>
+ this._handleSaveShortcut(e)
+ )
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: 's', modifiers: [Modifier.META_KEY]}, e =>
+ this._handleSaveShortcut(e)
+ )
+ );
}
override disconnectedCallback() {
this.storeTask?.cancel();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
@@ -394,7 +399,7 @@ export class GrEditorView extends base {
);
}
- _handleSaveShortcut(e: IronKeyboardEvent) {
+ _handleSaveShortcut(e: KeyboardEvent) {
e.preventDefault();
if (!this._saveDisabled) {
this._saveEdit();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index a629d0e9a4..e7137e4d9d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -20,12 +20,12 @@ import '../../../styles/shared-styles';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-autocomplete-dropdown_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {IronFitMixin} from '../../../mixins/iron-fit-mixin/iron-fit-mixin';
import {customElement, property, observe} from '@polymer/decorators';
import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior';
import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
import {fireEvent} from '../../../utils/event-util';
+import {addShortcut, Key} from '../../../utils/dom-util';
export interface GrAutocompleteDropdown {
$: {
@@ -53,10 +53,7 @@ export interface ItemSelectedEvent {
}
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = IronFitMixin(
- KeyboardShortcutMixin(PolymerElement),
- IronFitBehavior as IronFitBehavior
-);
+const base = IronFitMixin(PolymerElement, IronFitBehavior as IronFitBehavior);
@customElement('gr-autocomplete-dropdown')
export class GrAutocompleteDropdown extends base {
@@ -91,15 +88,8 @@ export class GrAutocompleteDropdown extends base {
@property({type: Array})
suggestions: Item[] = [];
- get keyBindings() {
- return {
- up: '_handleUp',
- down: '_handleDown',
- enter: '_handleEnter',
- esc: '_handleEscape',
- tab: '_handleTab',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
// visible for testing
cursor = new GrCursorManager();
@@ -110,8 +100,29 @@ export class GrAutocompleteDropdown extends base {
this.cursor.focusOnMove = true;
}
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ESC}, _ => this._handleEscape())
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+ );
+ }
+
override disconnectedCallback() {
this.cursor.unsetCursor();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index bb47dbc04f..86de3b3879 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -50,7 +50,7 @@ suite('gr-autocomplete-dropdown', () => {
test('escape key', () => {
const closeSpy = sinon.spy(element, 'close');
- MockInteractions.pressAndReleaseKeyOn(element, 27);
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
flush();
assert.isTrue(closeSpy.called);
});
@@ -59,7 +59,7 @@ suite('gr-autocomplete-dropdown', () => {
const handleTabSpy = sinon.spy(element, '_handleTab');
const itemSelectedStub = sinon.stub();
element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 9);
+ MockInteractions.pressAndReleaseKeyOn(element, 9, null, 'Tab');
assert.isTrue(handleTabSpy.called);
assert.equal(element.cursor.index, 0);
assert.isTrue(itemSelectedStub.called);
@@ -73,7 +73,7 @@ suite('gr-autocomplete-dropdown', () => {
const handleEnterSpy = sinon.spy(element, '_handleEnter');
const itemSelectedStub = sinon.stub();
element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 13);
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'Enter');
assert.isTrue(handleEnterSpy.called);
assert.equal(element.cursor.index, 0);
assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
@@ -85,11 +85,11 @@ suite('gr-autocomplete-dropdown', () => {
test('down key', () => {
element.isHidden = true;
const nextSpy = sinon.spy(element.cursor, 'next');
- MockInteractions.pressAndReleaseKeyOn(element, 40);
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
assert.isFalse(nextSpy.called);
assert.equal(element.cursor.index, 0);
element.isHidden = false;
- MockInteractions.pressAndReleaseKeyOn(element, 40);
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
assert.isTrue(nextSpy.called);
assert.equal(element.cursor.index, 1);
});
@@ -97,13 +97,13 @@ suite('gr-autocomplete-dropdown', () => {
test('up key', () => {
element.isHidden = true;
const prevSpy = sinon.spy(element.cursor, 'previous');
- MockInteractions.pressAndReleaseKeyOn(element, 38);
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
assert.isFalse(prevSpy.called);
assert.equal(element.cursor.index, 0);
element.isHidden = false;
element.cursor.setCursorAtIndex(1);
assert.equal(element.cursor.index, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
assert.isTrue(prevSpy.called);
assert.equal(element.cursor.index, 0);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 524b197742..8e84aa21e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -22,7 +22,6 @@ import '../../../styles/shared-styles';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-autocomplete_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {property, customElement, observe} from '@polymer/decorators';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElementExt} from '../../../types/types';
@@ -65,11 +64,8 @@ export interface AutocompleteCommitEventDetail {
export type AutocompleteCommitEvent =
CustomEvent<AutocompleteCommitEventDetail>;
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-autocomplete')
-export class GrAutocomplete extends base {
+export class GrAutocomplete extends PolymerElement {
static get template() {
return htmlTemplate;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index e7417f4364..6b2e5c4d04 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -70,7 +70,7 @@ import {TokenHighlightLayer} from '../../diff/gr-diff-builder/token-highlight-la
import {anyLineTooLong} from '../../diff/gr-diff/gr-diff-utils';
import {getUserName} from '../../../utils/display-name-util';
import {generateAbsoluteUrl} from '../../../utils/url-util';
-import {addShortcut} from '../../../utils/dom-util';
+import {addGlobalShortcut} from '../../../utils/dom-util';
const UNRESOLVED_EXPAND_COUNT = 5;
const NEWLINE_PATTERN = /\n/g;
@@ -84,8 +84,6 @@ export interface GrCommentThread {
@customElement('gr-comment-thread')
export class GrCommentThread extends PolymerElement {
- // KeyboardShortcutMixin Not used in this element rather other elements tests
-
static get template() {
return htmlTemplate;
}
@@ -239,10 +237,10 @@ export class GrCommentThread extends PolymerElement {
override connectedCallback() {
super.connectedCallback();
this.cleanups.push(
- addShortcut({key: 'e'}, e => this.handleExpandShortcut(e))
+ addGlobalShortcut({key: 'e'}, e => this.handleExpandShortcut(e))
);
this.cleanups.push(
- addShortcut({key: 'E'}, e => this.handleCollapseShortcut(e))
+ addGlobalShortcut({key: 'E'}, e => this.handleCollapseShortcut(e))
);
this._getLoggedIn().then(loggedIn => {
this._showActions = loggedIn;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 53b3bb9bb5..154a045193 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -30,7 +30,6 @@ import '../gr-account-label/gr-account-label';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-comment_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {getRootElement} from '../../../scripts/rootElement';
import {appContext} from '../../../services/app-context';
import {customElement, observe, property} from '@polymer/decorators';
@@ -60,6 +59,7 @@ import {pluralize} from '../../../utils/string-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {StorageLocation} from '../../../services/storage/gr-storage';
+import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
const STORAGE_DEBOUNCE_INTERVAL = 400;
const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -100,11 +100,8 @@ export interface GrComment {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-comment')
-export class GrComment extends base {
+export class GrComment extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -273,12 +270,8 @@ export class GrComment extends base {
@property({type: Boolean})
showPortedComment = false;
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
- esc: '_handleEsc',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
private readonly restApiService = appContext.restApiService;
@@ -307,9 +300,21 @@ export class GrComment extends base {
this._getIsAdmin().then(isAdmin => {
this._isAdmin = !!isAdmin;
});
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ESC}, e => this._handleEsc(e))
+ );
+ for (const key of ['s', Key.ENTER]) {
+ for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
+ addShortcut(this, {key, modifiers: [modifier]}, e =>
+ this._handleSaveKey(e)
+ );
+ }
+ }
}
override disconnectedCallback() {
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
this.fireUpdateTask?.cancel();
this.storeTask?.cancel();
this.draftToastTask?.cancel();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 53e62ff600..00edc07fc3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -322,43 +322,43 @@ suite('gr-comment tests', () => {
});
test('esc closes comment when text is empty', () => {
- pressAndReleaseKeyOn(element.textarea!, 27); // esc
+ pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
assert.isTrue(handleCancelStub.called);
});
test('ctrl+enter does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl'); // ctrl + enter
+ pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl', 'Enter');
assert.isFalse(handleSaveStub.called);
});
test('meta+enter does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'meta'); // meta + enter
+ pressAndReleaseKeyOn(element.textarea!, 13, 'meta', 'Enter');
assert.isFalse(handleSaveStub.called);
});
test('ctrl+s does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl'); // ctrl + s
+ pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl', 's');
assert.isFalse(handleSaveStub.called);
});
});
test('esc does not close comment that has content', () => {
- pressAndReleaseKeyOn(element.textarea!, 27); // esc
+ pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
assert.isFalse(handleCancelStub.called);
});
test('ctrl+enter saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl'); // ctrl + enter
+ pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl', 'Enter');
assert.isTrue(handleSaveStub.called);
});
test('meta+enter saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'meta'); // meta + enter
+ pressAndReleaseKeyOn(element.textarea!, 13, 'meta', 'Enter');
assert.isTrue(handleSaveStub.called);
});
test('ctrl+s saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl'); // ctrl + s
+ pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl', 's');
assert.isTrue(handleSaveStub.called);
});
});
@@ -1015,7 +1015,7 @@ suite('gr-comment tests', () => {
element._messageText = '';
element.editing = true;
await flush();
- pressAndReleaseKeyOn(element.textarea!, 27); // esc
+ pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
await promise;
});
@@ -1093,7 +1093,12 @@ suite('gr-comment tests', () => {
element._messageText = 'is that the horse from horsing around??';
element.editing = true;
await flush();
- pressAndReleaseKeyOn(element.textarea!.$.textarea.textarea, 83, 'ctrl'); // 'ctrl + s'
+ pressAndReleaseKeyOn(
+ element.textarea!.$.textarea.textarea,
+ 83,
+ 'ctrl',
+ 's'
+ );
await promise;
});
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index f4179f4497..2b56de65d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -24,10 +24,10 @@ import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-dropdown_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
import {property, customElement, observe} from '@polymer/decorators';
+import {addShortcut, Key} from '../../../utils/dom-util';
const REL_NOOPENER = 'noopener';
const REL_EXTERNAL = 'external';
@@ -67,11 +67,8 @@ export interface DropdownContent {
bold?: boolean;
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-dropdown')
-export class GrDropdown extends base {
+export class GrDropdown extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -121,14 +118,8 @@ export class GrDropdown extends base {
@property({type: Array})
disabledIds: string[] = [];
- get keyBindings() {
- return {
- down: '_handleDown',
- 'enter space': '_handleEnter',
- tab: '_handleTab',
- up: '_handleUp',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
// Used within the tests so needs to be non-private.
cursor = new GrCursorManager();
@@ -139,15 +130,36 @@ export class GrDropdown extends base {
this.cursor.focusOnMove = true;
}
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.SPACE}, e => this._handleEnter(e))
+ );
+ }
+
override disconnectedCallback() {
this.cursor.unsetCursor();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
super.disconnectedCallback();
}
/**
* Handle the up key.
*/
- _handleUp(e: MouseEvent) {
+ _handleUp(e: Event) {
if (this.$.dropdown.opened) {
e.preventDefault();
e.stopPropagation();
@@ -160,7 +172,7 @@ export class GrDropdown extends base {
/**
* Handle the down key.
*/
- _handleDown(e: MouseEvent) {
+ _handleDown(e: Event) {
if (this.$.dropdown.opened) {
e.preventDefault();
e.stopPropagation();
@@ -173,7 +185,7 @@ export class GrDropdown extends base {
/**
* Handle the tab key.
*/
- _handleTab(e: MouseEvent) {
+ _handleTab(e: Event) {
if (this.$.dropdown.opened) {
// Tab in a native select is a no-op. Emulate this.
e.preventDefault();
@@ -184,7 +196,7 @@ export class GrDropdown extends base {
/**
* Handle the enter key.
*/
- _handleEnter(e: MouseEvent) {
+ _handleEnter(e: Event) {
e.preventDefault();
e.stopPropagation();
if (this.$.dropdown.opened) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
index e14d523bae..393f44e9e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
@@ -170,18 +170,18 @@ suite('gr-dropdown tests', () => {
test('down', () => {
const stub = sinon.stub(element.cursor, 'next');
assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'ArrowDown');
assert.isTrue(stub.called);
});
test('up', () => {
const stub = sinon.stub(element.cursor, 'previous');
assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'ArrowUp');
assert.isTrue(stub.called);
});
@@ -189,7 +189,7 @@ suite('gr-dropdown tests', () => {
// Because enter and space are handled by the same fn, we need only to
// test one.
assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ MockInteractions.pressAndReleaseKeyOn(element, 32, null, ' ');
assert.isTrue(element.$.dropdown.opened);
const el = queryAndAssert<HTMLAnchorElement>(
@@ -197,7 +197,7 @@ suite('gr-dropdown tests', () => {
':not([hidden]) a'
);
const stub = sinon.stub(el, 'click');
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ MockInteractions.pressAndReleaseKeyOn(element, 32, null, ' ');
assert.isTrue(stub.called);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index 13b195e47f..afb695f8ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -20,17 +20,16 @@ import '../../../styles/shared-styles';
import '../gr-button/gr-button';
import '../../shared/gr-autocomplete/gr-autocomplete';
import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {customElement, property} from '@polymer/decorators';
import {htmlTemplate} from './gr-editable-label_html';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PaperInputElementExt} from '../../../types/types';
-import {IronKeyboardEvent} from '../../../types/events';
import {
AutocompleteQuery,
GrAutocomplete,
} from '../gr-autocomplete/gr-autocomplete';
+import {addShortcut, Key} from '../../../utils/dom-util';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -47,11 +46,8 @@ export interface GrEditableLabel {
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-editable-label')
-export class GrEditableLabel extends base {
+export class GrEditableLabel extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -106,11 +102,23 @@ export class GrEditableLabel extends base {
this._ensureAttribute('tabindex', '0');
}
- get keyBindings() {
- return {
- enter: '_handleEnter',
- esc: '_handleEsc',
- };
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ESC}, e => this._handleEsc(e))
+ );
}
_usePlaceholder(value?: string, placeholder?: string) {
@@ -204,8 +212,7 @@ export class GrEditableLabel extends base {
this.getGrAutocomplete()) as HTMLInputElement;
}
- _handleEnter(event: IronKeyboardEvent) {
- const e = event.detail.keyboardEvent;
+ _handleEnter(e: KeyboardEvent) {
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
@@ -213,8 +220,7 @@ export class GrEditableLabel extends base {
}
}
- _handleEsc(event: IronKeyboardEvent) {
- const e = event.detail.keyboardEvent;
+ _handleEsc(e: KeyboardEvent) {
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
index 3e217f0566..b6bb87b564 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.js
@@ -101,7 +101,7 @@ suite('gr-editable-label tests', () => {
element._inputText = 'new text';
// Press enter:
- MockInteractions.keyDownOn(input, 13);
+ MockInteractions.keyDownOn(input, 13, null, 'Enter');
flush();
assert.isTrue(editedSpy.called);
@@ -122,7 +122,7 @@ suite('gr-editable-label tests', () => {
element._inputText = 'new text';
// Press enter:
- MockInteractions.tap(element.$.saveBtn, 13);
+ MockInteractions.tap(element.$.saveBtn, 13, null, 'Enter');
flush();
assert.isTrue(editedSpy.called);
@@ -143,7 +143,7 @@ suite('gr-editable-label tests', () => {
element._inputText = 'new text';
// Press escape:
- MockInteractions.keyDownOn(input, 27);
+ MockInteractions.keyDownOn(input, 27, null, 'Escape');
flush();
assert.isFalse(editedSpy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 434da1f3e8..337d59515c 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -23,7 +23,6 @@ import '../../../styles/shared-styles';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-textarea_html';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {appContext} from '../../../services/app-context';
import {customElement, property} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
@@ -33,7 +32,7 @@ import {
Item,
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {IronKeyboardEvent} from '../../../types/events';
+import {addShortcut, Key} from '../../../utils/dom-util';
const MAX_ITEMS_DROPDOWN = 10;
@@ -85,11 +84,8 @@ declare global {
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-textarea')
-export class GrTextarea extends base {
+export class GrTextarea extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -150,21 +146,39 @@ export class GrTextarea extends base {
disableEnterKeyForSelectingEmoji = false;
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- tab: '_handleTabKey',
- enter: '_handleEnterByKey',
- up: '_handleUpKey',
- down: '_handleDownKey',
- };
- }
+ /** Called in disconnectedCallback. */
+ private cleanups: (() => void)[] = [];
constructor() {
super();
this.reporting = appContext.reportingService;
}
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const cleanup of this.cleanups) cleanup();
+ this.cleanups = [];
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.cleanups.push(
+ addShortcut(this, {key: Key.UP}, e => this._handleUpKey(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.DOWN}, e => this._handleDownKey(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.TAB}, e => this._handleTabKey(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER}, e => this._handleEnterByKey(e))
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ESC}, e => this._handleEscKey(e))
+ );
+ }
+
override ready() {
super.ready();
if (this.monospace) {
@@ -238,16 +252,11 @@ export class GrTextarea extends base {
this._setEmoji(this.$.emojiSuggestions.getCurrentText());
}
- _handleEnterByKey(e: IronKeyboardEvent) {
+ _handleEnterByKey(e: KeyboardEvent) {
// Enter should have newline behavior if the picker is closed or if the user
// has only typed ':'. Also make sure that shortcuts aren't clobbered.
if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
- if (
- !e.detail.keyboardEvent?.metaKey &&
- !e.detail.keyboardEvent?.ctrlKey
- ) {
- this.indent(e);
- }
+ this.indent(e);
return;
}
@@ -420,7 +429,7 @@ export class GrTextarea extends base {
);
}
- private indent(e: IronKeyboardEvent): void {
+ private indent(e: KeyboardEvent): void {
if (!document.queryCommandSupported('insertText')) {
return;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 7e596927fa..318c72024b 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -20,7 +20,6 @@ import './gr-textarea';
import {GrTextarea} from './gr-textarea';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {IronKeyboardEvent} from '../../../types/events';
import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
const basicFixture = fixtureFromElement('gr-textarea');
@@ -238,34 +237,12 @@ suite('gr-textarea tests', () => {
const indentCommand = sinon.stub(document, 'execCommand');
element.$.textarea.value = ' a';
element._handleEnterByKey(
- new CustomEvent('keydown', {
- detail: {keyboardEvent: {keyCode: 13}},
- }) as IronKeyboardEvent
+ new KeyboardEvent('keydown', {key: 'Enter', keyCode: 13})
);
await flush();
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
});
- test('ctrl+enter and meta+enter do not indent', async () => {
- const indentCommand = sinon.stub(document, 'execCommand');
- element.$.textarea.value = ' a';
- element._handleEnterByKey(
- new CustomEvent('keydown', {
- detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
- }) as IronKeyboardEvent
- );
- await flush();
- assert.isTrue(indentCommand.notCalled);
-
- element._handleEnterByKey(
- new CustomEvent('keydown', {
- detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
- }) as IronKeyboardEvent
- );
- await flush();
- assert.isTrue(indentCommand.notCalled);
- });
-
test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
element.$.emojiSuggestions.dispatchEvent(
@@ -301,38 +278,78 @@ suite('gr-textarea tests', () => {
test('escape key', () => {
const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 27,
+ null,
+ 'Escape'
+ );
assert.isFalse(resetSpy.called);
setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 27,
+ null,
+ 'Escape'
+ );
assert.isTrue(resetSpy.called);
assert.isFalse(!element.$.emojiSuggestions.isHidden);
});
test('up key', () => {
const upSpy = sinon.spy(element.$.emojiSuggestions, 'cursorUp');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 38,
+ null,
+ 'ArrowUp'
+ );
assert.isFalse(upSpy.called);
setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 38,
+ null,
+ 'ArrowUp'
+ );
assert.isTrue(upSpy.called);
});
test('down key', () => {
const downSpy = sinon.spy(element.$.emojiSuggestions, 'cursorDown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 40,
+ null,
+ 'ArrowDown'
+ );
assert.isFalse(downSpy.called);
setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 40,
+ null,
+ 'ArrowDown'
+ );
assert.isTrue(downSpy.called);
});
test('enter key', () => {
const enterSpy = sinon.spy(element.$.emojiSuggestions, 'getCursorTarget');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 13,
+ null,
+ 'Enter'
+ );
assert.isFalse(enterSpy.called);
setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.textarea,
+ 13,
+ null,
+ 'Enter'
+ );
assert.isTrue(enterSpy.called);
flush();
assert.equal(element.text, '💯');
diff --git a/polygerrit-ui/app/test/common-test-setup-karma.ts b/polygerrit-ui/app/test/common-test-setup-karma.ts
index 3463d3b3f4..39c79d1112 100644
--- a/polygerrit-ui/app/test/common-test-setup-karma.ts
+++ b/polygerrit-ui/app/test/common-test-setup-karma.ts
@@ -118,7 +118,7 @@ interface TagTestFixture<T extends Element> {
}
class TestFixture {
- constructor(private readonly fixtureId: string) {}
+ constructor(readonly fixtureId: string) {}
/**
* Create an instance of a fixture's template.
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index e3e342e50a..a7ad6e1119 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -327,29 +327,76 @@ export enum Modifier {
}
export interface Shortcut {
- key: string;
+ key: string | Key;
modifiers?: Modifier[];
}
+const ALPHA_NUM = new RegExp(/^[A-Za-z0-9]$/);
+
+/**
+ * For "normal" keys we do not check that the SHIFT modifier is pressed or not,
+ * because that depends on the keyboard layout. Just checking the key string is
+ * sufficient.
+ *
+ * But for some special keys it is important whether SHIFT is pressed at the
+ * same time, for example we want to distinguish Enter from Shift+Enter.
+ */
+function shiftMustMatch(key: string | Key) {
+ return Object.values(Key).includes(key as Key);
+}
+
+/**
+ * For a-zA-Z0-9 and for Enter, Tab, etc. we want to check the ALT modifier.
+ *
+ * But for special chars like []/? we don't care whether the user is pressing
+ * the ALT modifier to produce the special char. For example on a German
+ * keyboard layout you have to press ALT to produce a [.
+ */
+function altMustMatch(key: string | Key) {
+ return ALPHA_NUM.test(key) || Object.values(Key).includes(key as Key);
+}
+
+export function eventMatchesShortcut(
+ e: KeyboardEvent,
+ shortcut: Shortcut
+): boolean {
+ if (e.key !== shortcut.key) return false;
+ const modifiers = shortcut.modifiers ?? [];
+ if (e.ctrlKey !== modifiers.includes(Modifier.CTRL_KEY)) return false;
+ if (e.metaKey !== modifiers.includes(Modifier.META_KEY)) return false;
+ if (
+ altMustMatch(e.key) &&
+ e.altKey !== modifiers.includes(Modifier.ALT_KEY)
+ ) {
+ return false;
+ }
+ if (
+ shiftMustMatch(e.key) &&
+ e.shiftKey !== modifiers.includes(Modifier.SHIFT_KEY)
+ ) {
+ return false;
+ }
+ return true;
+}
+
+export function addGlobalShortcut(
+ shortcut: Shortcut,
+ listener: (e: KeyboardEvent) => void
+) {
+ return addShortcut(document.body, shortcut, listener);
+}
+
export function addShortcut(
+ element: HTMLElement,
shortcut: Shortcut,
listener: (e: KeyboardEvent) => void
) {
const wrappedListener = (e: KeyboardEvent) => {
- if (e.key !== shortcut.key) return;
- const modifiers = shortcut.modifiers ?? [];
- if (e.ctrlKey !== modifiers.includes(Modifier.CTRL_KEY)) return;
- if (e.metaKey !== modifiers.includes(Modifier.META_KEY)) return;
- // TODO(brohlfs): Refine the matching of modifiers. For "normal" keys we
- // don't want to check ALT and SHIFT, because we don't know what the
- // keyboard layout looks like. The user may have to use ALT and/or SHIFT for
- // certain keys. Comparing the `key` string is sufficient in that case.
- // if (e.altKey !== modifiers.includes(Modifier.ALT_KEY)) return;
- // if (e.shiftKey !== modifiers.includes(Modifier.SHIFT_KEY)) return;
- listener(e);
+ if (e.repeat) return;
+ if (eventMatchesShortcut(e, shortcut)) listener(e);
};
- document.addEventListener('keydown', wrappedListener);
- return () => document.removeEventListener('keydown', wrappedListener);
+ element.addEventListener('keydown', wrappedListener);
+ return () => element.removeEventListener('keydown', wrappedListener);
}
export function modifierPressed(e: KeyboardEvent) {
diff --git a/polygerrit-ui/app/utils/dom-util_test.js b/polygerrit-ui/app/utils/dom-util_test.js
deleted file mode 100644
index bcb45051ab..0000000000
--- a/polygerrit-ui/app/utils/dom-util_test.js
+++ /dev/null
@@ -1,155 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../test/common-test-setup-karma.js';
-import {strToClassName, getComputedStyleValue, querySelector, querySelectorAll, descendedFromClass, getEventPath} from './dom-util.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-class TestEle extends PolymerElement {
- static get is() {
- return 'dom-util-test-element';
- }
-
- static get template() {
- return html`
- <div>
- <div class="a">
- <div class="b">
- <div class="c"></div>
- </div>
- <span class="ss"></span>
- </div>
- <span class="ss"></span>
- </div>
- `;
- }
-}
-
-customElements.define(TestEle.is, TestEle);
-
-const basicFixture = fixtureFromTemplate(html`
- <div id="test" class="a b c">
- <a class="testBtn" style="color:red;"></a>
- <dom-util-test-element></dom-util-test-element>
- <span class="ss"></span>
- </div>
-`);
-
-suite('dom-util tests', () => {
- suite('getEventPath', () => {
- test('empty event', () => {
- assert.equal(getEventPath(), '');
- assert.equal(getEventPath(null), '');
- assert.equal(getEventPath(undefined), '');
- assert.equal(getEventPath({composedPath: () => []}), '');
- });
-
- test('event with fake path', () => {
- assert.equal(getEventPath({composedPath: () => []}), '');
- const dd = document.createElement('dd');
- assert.equal(getEventPath({composedPath: () => [dd]}), 'dd');
- });
-
- test('event with fake complicated path', () => {
- const dd = document.createElement('dd');
- dd.setAttribute('id', 'test');
- dd.className = 'a b';
- const divNode = document.createElement('DIV');
- divNode.id = 'test2';
- divNode.className = 'a b c';
- assert.equal(getEventPath(
- {composedPath: () => [dd, divNode]}),
- 'div#test2.a.b.c>dd#test.a.b'
- );
- });
-
- test('event with fake target', () => {
- const fakeTargetParent1 = document.createElement('dd');
- fakeTargetParent1.setAttribute('id', 'test');
- fakeTargetParent1.className = 'a b';
- const fakeTargetParent2 = document.createElement('DIV');
- fakeTargetParent2.id = 'test2';
- fakeTargetParent2.className = 'a b c';
- fakeTargetParent2.appendChild(fakeTargetParent1);
- const fakeTarget = document.createElement('SPAN');
- fakeTargetParent1.appendChild(fakeTarget);
- assert.equal(
- getEventPath({composedPath: () => {}, target: fakeTarget}),
- 'div#test2.a.b.c>dd#test.a.b>span'
- );
- });
-
- test('event with real click', () => {
- const element = basicFixture.instantiate();
- const aLink = element.querySelector('a');
- let path;
- aLink.onclick = e => path = getEventPath(e);
- MockInteractions.click(aLink);
- assert.equal(
- path,
- `html>body>test-fixture#${basicFixture.fixtureId}>` +
- 'div#test.a.b.c>a.testBtn'
- );
- });
- });
-
- suite('querySelector and querySelectorAll', () => {
- test('query cross shadow dom', () => {
- const element = basicFixture.instantiate();
- const theFirstEl = querySelector(element, '.ss');
- const allEls = querySelectorAll(element, '.ss');
- assert.equal(allEls.length, 3);
- assert.equal(theFirstEl, allEls[0]);
- });
- });
-
- suite('getComputedStyleValue', () => {
- test('color style', () => {
- const element = basicFixture.instantiate();
- const testBtn = querySelector(element, '.testBtn');
- assert.equal(
- getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)'
- );
- });
- });
-
- suite('descendedFromClass', () => {
- test('basic tests', () => {
- const element = basicFixture.instantiate();
- const testEl = querySelector(element, 'dom-util-test-element');
- // .c is a child of .a and not vice versa.
- assert.isTrue(descendedFromClass(querySelector(testEl, '.c'), 'a'));
- assert.isFalse(descendedFromClass(querySelector(testEl, '.a'), 'c'));
-
- // Stops at stop element.
- assert.isFalse(descendedFromClass(querySelector(testEl, '.c'), 'a',
- querySelector(testEl, '.b')));
- });
- });
-
- suite('strToClassName', () => {
- test('basic tests', () => {
- assert.equal(strToClassName(''), 'generated_');
- assert.equal(strToClassName('11'), 'generated_11');
- assert.equal(strToClassName('0.123'), 'generated_0_123');
- assert.equal(strToClassName('0.123', 'prefix_'), 'prefix_0_123');
- assert.equal(strToClassName('0>123', 'prefix_'), 'prefix_0_123');
- assert.equal(strToClassName('0<123', 'prefix_'), 'prefix_0_123');
- assert.equal(strToClassName('0+1+23', 'prefix_'), 'prefix_0_1_23');
- });
- });
-}); \ No newline at end of file
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
new file mode 100644
index 0000000000..2993c0ebfa
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -0,0 +1,269 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../test/common-test-setup-karma';
+import {
+ descendedFromClass,
+ eventMatchesShortcut,
+ getComputedStyleValue,
+ getEventPath,
+ Modifier,
+ querySelectorAll,
+ strToClassName,
+} from './dom-util';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {queryAndAssert} from '../test/test-utils';
+
+class TestEle extends PolymerElement {
+ static get is() {
+ return 'dom-util-test-element';
+ }
+
+ static get template() {
+ return html`
+ <div>
+ <div class="a">
+ <div class="b">
+ <div class="c"></div>
+ </div>
+ <span class="ss"></span>
+ </div>
+ <span class="ss"></span>
+ </div>
+ `;
+ }
+}
+
+customElements.define(TestEle.is, TestEle);
+
+const basicFixture = fixtureFromTemplate(html`
+ <div id="test" class="a b c">
+ <a class="testBtn" style="color:red;"></a>
+ <dom-util-test-element></dom-util-test-element>
+ <span class="ss"></span>
+ </div>
+`);
+
+suite('dom-util tests', () => {
+ suite('getEventPath', () => {
+ test('empty event', () => {
+ assert.equal(getEventPath(), '');
+ assert.equal(getEventPath(undefined), '');
+ assert.equal(getEventPath(new MouseEvent('click')), '');
+ });
+
+ test('event with fake path', () => {
+ assert.equal(getEventPath(new MouseEvent('click')), '');
+ const dd = document.createElement('dd');
+ assert.equal(
+ getEventPath({...new MouseEvent('click'), composedPath: () => [dd]}),
+ 'dd'
+ );
+ });
+
+ test('event with fake complicated path', () => {
+ const dd = document.createElement('dd');
+ dd.setAttribute('id', 'test');
+ dd.className = 'a b';
+ const divNode = document.createElement('DIV');
+ divNode.id = 'test2';
+ divNode.className = 'a b c';
+ assert.equal(
+ getEventPath({
+ ...new MouseEvent('click'),
+ composedPath: () => [dd, divNode],
+ }),
+ 'div#test2.a.b.c>dd#test.a.b'
+ );
+ });
+
+ test('event with fake target', () => {
+ const fakeTargetParent1 = document.createElement('dd');
+ fakeTargetParent1.setAttribute('id', 'test');
+ fakeTargetParent1.className = 'a b';
+ const fakeTargetParent2 = document.createElement('DIV');
+ fakeTargetParent2.id = 'test2';
+ fakeTargetParent2.className = 'a b c';
+ fakeTargetParent2.appendChild(fakeTargetParent1);
+ const fakeTarget = document.createElement('SPAN');
+ fakeTargetParent1.appendChild(fakeTarget);
+ assert.equal(
+ getEventPath({
+ ...new MouseEvent('click'),
+ composedPath: () => [],
+ target: fakeTarget,
+ }),
+ 'div#test2.a.b.c>dd#test.a.b>span'
+ );
+ });
+
+ test('event with real click', () => {
+ const element = basicFixture.instantiate() as HTMLElement;
+ const aLink = queryAndAssert(element, 'a');
+ let path;
+ aLink.addEventListener('click', (e: Event) => {
+ path = getEventPath(e as MouseEvent);
+ });
+ MockInteractions.click(aLink);
+ assert.equal(
+ path,
+ `html>body>test-fixture#${basicFixture.fixtureId}>` +
+ 'div#test.a.b.c>a.testBtn'
+ );
+ });
+ });
+
+ suite('querySelector and querySelectorAll', () => {
+ test('query cross shadow dom', () => {
+ const element = basicFixture.instantiate() as HTMLElement;
+ const theFirstEl = queryAndAssert(element, '.ss');
+ const allEls = querySelectorAll(element, '.ss');
+ assert.equal(allEls.length, 3);
+ assert.equal(theFirstEl, allEls[0]);
+ });
+ });
+
+ suite('getComputedStyleValue', () => {
+ test('color style', () => {
+ const element = basicFixture.instantiate() as HTMLElement;
+ const testBtn = queryAndAssert(element, '.testBtn');
+ assert.equal(getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)');
+ });
+ });
+
+ suite('descendedFromClass', () => {
+ test('basic tests', () => {
+ const element = basicFixture.instantiate() as HTMLElement;
+ const testEl = queryAndAssert(element, 'dom-util-test-element');
+ // .c is a child of .a and not vice versa.
+ assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.c'), 'a'));
+ assert.isFalse(descendedFromClass(queryAndAssert(testEl, '.a'), 'c'));
+
+ // Stops at stop element.
+ assert.isFalse(
+ descendedFromClass(
+ queryAndAssert(testEl, '.c'),
+ 'a',
+ queryAndAssert(testEl, '.b')
+ )
+ );
+ });
+ });
+
+ suite('strToClassName', () => {
+ test('basic tests', () => {
+ assert.equal(strToClassName(''), 'generated_');
+ assert.equal(strToClassName('11'), 'generated_11');
+ assert.equal(strToClassName('0.123'), 'generated_0_123');
+ assert.equal(strToClassName('0.123', 'prefix_'), 'prefix_0_123');
+ assert.equal(strToClassName('0>123', 'prefix_'), 'prefix_0_123');
+ assert.equal(strToClassName('0<123', 'prefix_'), 'prefix_0_123');
+ assert.equal(strToClassName('0+1+23', 'prefix_'), 'prefix_0_1_23');
+ });
+ });
+
+ suite('eventMatchesShortcut', () => {
+ test('basic tests', () => {
+ const a = new KeyboardEvent('keydown', {key: 'a'});
+ const b = new KeyboardEvent('keydown', {key: 'B'});
+ assert.isTrue(eventMatchesShortcut(a, {key: 'a'}));
+ assert.isFalse(eventMatchesShortcut(a, {key: 'B'}));
+ assert.isFalse(eventMatchesShortcut(b, {key: 'a'}));
+ assert.isTrue(eventMatchesShortcut(b, {key: 'B'}));
+ });
+
+ test('check modifiers for a', () => {
+ const e = new KeyboardEvent('keydown', {key: 'a'});
+ const s = {key: 'a'};
+ assert.isTrue(eventMatchesShortcut(e, s));
+
+ const eAlt = new KeyboardEvent('keydown', {key: 'a', altKey: true});
+ const sAlt = {key: 'a', modifiers: [Modifier.ALT_KEY]};
+ assert.isFalse(eventMatchesShortcut(eAlt, s));
+ assert.isFalse(eventMatchesShortcut(e, sAlt));
+ const eCtrl = new KeyboardEvent('keydown', {key: 'a', ctrlKey: true});
+ const sCtrl = {key: 'a', modifiers: [Modifier.CTRL_KEY]};
+ assert.isFalse(eventMatchesShortcut(eCtrl, s));
+ assert.isFalse(eventMatchesShortcut(e, sCtrl));
+ const eMeta = new KeyboardEvent('keydown', {key: 'a', metaKey: true});
+ const sMeta = {key: 'a', modifiers: [Modifier.META_KEY]};
+ assert.isFalse(eventMatchesShortcut(eMeta, s));
+ assert.isFalse(eventMatchesShortcut(e, sMeta));
+
+ // Do NOT check SHIFT for alphanum keys.
+ const eShift = new KeyboardEvent('keydown', {key: 'a', shiftKey: true});
+ const sShift = {key: 'a', modifiers: [Modifier.SHIFT_KEY]};
+ assert.isTrue(eventMatchesShortcut(eShift, s));
+ assert.isTrue(eventMatchesShortcut(e, sShift));
+ });
+
+ test('check modifiers for Enter', () => {
+ const e = new KeyboardEvent('keydown', {key: 'Enter'});
+ const s = {key: 'Enter'};
+ assert.isTrue(eventMatchesShortcut(e, s));
+
+ const eAlt = new KeyboardEvent('keydown', {key: 'Enter', altKey: true});
+ const sAlt = {key: 'Enter', modifiers: [Modifier.ALT_KEY]};
+ assert.isFalse(eventMatchesShortcut(eAlt, s));
+ assert.isFalse(eventMatchesShortcut(e, sAlt));
+ const eCtrl = new KeyboardEvent('keydown', {key: 'Enter', ctrlKey: true});
+ const sCtrl = {key: 'Enter', modifiers: [Modifier.CTRL_KEY]};
+ assert.isFalse(eventMatchesShortcut(eCtrl, s));
+ assert.isFalse(eventMatchesShortcut(e, sCtrl));
+ const eMeta = new KeyboardEvent('keydown', {key: 'Enter', metaKey: true});
+ const sMeta = {key: 'Enter', modifiers: [Modifier.META_KEY]};
+ assert.isFalse(eventMatchesShortcut(eMeta, s));
+ assert.isFalse(eventMatchesShortcut(e, sMeta));
+ const eShift = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ shiftKey: true,
+ });
+ const sShift = {key: 'Enter', modifiers: [Modifier.SHIFT_KEY]};
+ assert.isFalse(eventMatchesShortcut(eShift, s));
+ assert.isFalse(eventMatchesShortcut(e, sShift));
+ });
+
+ test('check modifiers for [', () => {
+ const e = new KeyboardEvent('keydown', {key: '['});
+ const s = {key: '['};
+ assert.isTrue(eventMatchesShortcut(e, s));
+
+ const eCtrl = new KeyboardEvent('keydown', {key: '[', ctrlKey: true});
+ const sCtrl = {key: '[', modifiers: [Modifier.CTRL_KEY]};
+ assert.isFalse(eventMatchesShortcut(eCtrl, s));
+ assert.isFalse(eventMatchesShortcut(e, sCtrl));
+ const eMeta = new KeyboardEvent('keydown', {key: '[', metaKey: true});
+ const sMeta = {key: '[', modifiers: [Modifier.META_KEY]};
+ assert.isFalse(eventMatchesShortcut(eMeta, s));
+ assert.isFalse(eventMatchesShortcut(e, sMeta));
+
+ // Do NOT check SHIFT and ALT for special chars like [.
+ const eAlt = new KeyboardEvent('keydown', {key: '[', altKey: true});
+ const sAlt = {key: '[', modifiers: [Modifier.ALT_KEY]};
+ assert.isTrue(eventMatchesShortcut(eAlt, s));
+ assert.isTrue(eventMatchesShortcut(e, sAlt));
+ const eShift = new KeyboardEvent('keydown', {
+ key: '[',
+ shiftKey: true,
+ });
+ const sShift = {key: '[', modifiers: [Modifier.SHIFT_KEY]};
+ assert.isTrue(eventMatchesShortcut(eShift, s));
+ assert.isTrue(eventMatchesShortcut(e, sShift));
+ });
+ });
+});