diff options
author | Ben Rohlfs <brohlfs@google.com> | 2021-10-21 14:29:01 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-10-21 14:29:01 +0000 |
commit | 5b8cc9e26c134bde649e4c168c360b917df49e22 (patch) | |
tree | fa19525efbb41dd608419cfa5927477c35e31e5a | |
parent | 242b1330bea7f70faf55dd0673760261c8935613 (diff) | |
parent | 771d18a0162fd37f3fae010eeb7a569e57cfb271 (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
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)); + }); + }); +}); |