You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ze...@apache.org on 2026/07/28 11:17:10 UTC
(streampipes) branch shift-key-navigation-prototype created (now 7f56423ee5)
This is an automated email from the ASF dual-hosted git repository.
tenthe pushed a change to branch shift-key-navigation-prototype
in repository https://gitbox.apache.org/repos/asf/streampipes.git
at 7f56423ee5 feat(#4695): add Shift key tips for navigation
This branch includes the following new commits:
new 7f56423ee5 feat(#4695): add Shift key tips for navigation
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(streampipes) 01/01: feat(#4695): add Shift key tips for navigation
Posted by ze...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tenthe pushed a commit to branch shift-key-navigation-prototype
in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 7f56423ee52435eec0196d6e50b8b69090ca0db1
Author: Philipp Zehnder <te...@users.noreply.github.com>
AuthorDate: Tue Jul 28 11:11:43 2026 +0000
feat(#4695): add Shift key tips for navigation
---
ui/deployment/base-navigation.component.mst | 7 +-
ui/deployment/modules.yml | 8 +-
.../src/lib/services/keyboard-shortcut.service.ts | 98 ++++++++++++++++++++--
.../core/components/iconbar/iconbar.component.html | 19 ++++-
.../core/components/iconbar/iconbar.component.scss | 27 ++++++
.../core/components/iconbar/iconbar.component.ts | 2 +
.../streampipes/streampipes.component.ts | 43 +++++-----
.../shortcuts/shortcut-dialog.component.html | 30 +++++++
.../shortcuts/shortcut-dialog.component.scss | 35 ++++++++
.../shortcuts/shortcut-dialog.component.ts} | 35 +++-----
.../components/shortcuts/shortcuts.component.ts | 10 +--
ui/src/app/keyboard-shortcut.service.spec.ts | 90 ++++++++++++++++++++
12 files changed, 343 insertions(+), 61 deletions(-)
diff --git a/ui/deployment/base-navigation.component.mst b/ui/deployment/base-navigation.component.mst
index 3e84aa1716..1faa094797 100644
--- a/ui/deployment/base-navigation.component.mst
+++ b/ui/deployment/base-navigation.component.mst
@@ -20,7 +20,10 @@
import { NavigationEnd, Router } from '@angular/router';
import { PageName } from '../navigation/page-name.enum';
import { AuthService } from '../../services/auth.service';
-import { CurrentUserService } from '@streampipes/shared-ui';
+import {
+ CurrentUserService,
+ KeyboardShortcutService
+} from '@streampipes/shared-ui';
import { AppConstants } from '../../services/app.constants';
import { UserPrivilege } from '../auth/user-privilege.enum';
import { UserRole } from '../auth/user-role.enum';
@@ -77,8 +80,10 @@ export abstract class BaseNavigationComponent {
protected router = inject(Router);
protected appConstants = inject(AppConstants);
protected collapseService = inject(CollapseService);
+ protected keyboardShortcutService = inject(KeyboardShortcutService);
collapsed = this.collapseService.isCollapsed;
+ shortcutHintsVisible = this.keyboardShortcutService.shortcutHintsVisible;
onInit() {
this.currentUserService.user$.subscribe(user => {
diff --git a/ui/deployment/modules.yml b/ui/deployment/modules.yml
index fd2a777aeb..c9e2b2b510 100644
--- a/ui/deployment/modules.yml
+++ b/ui/deployment/modules.yml
@@ -23,7 +23,7 @@ spAssets:
pageNames: 'PageName.ASSETS'
showStatusBox: false
category: 'management'
- shortcutKey: 'a'
+ shortcutKey: 'x'
spDatasets:
routesPath: './dataset/dataset.routes'
routesName: DATASET_ROUTES
@@ -34,7 +34,7 @@ spDatasets:
pageNames: 'PageName.DATASETS'
showStatusBox: false
category: 'management'
- shortcutKey: 'y'
+ shortcutKey: 't'
spHome:
routesPath: './home/home.routes'
routesName: HOME_ROUTES
@@ -66,7 +66,7 @@ spConnect:
viewRoles: '[UserPrivilege.PRIVILEGE_READ_ADAPTER, UserPrivilege.PRIVILEGE_WRITE_ADAPTER]'
createRoles: '[UserPrivilege.PRIVILEGE_WRITE_ADAPTER]'
category: 'management'
- shortcutKey: 'c'
+ shortcutKey: 'a'
spPipelines:
routesPath: './pipelines/pipelines.routes'
routesName: PIPELINES_ROUTES
@@ -138,4 +138,4 @@ spChart:
viewRoles: '[UserPrivilege.PRIVILEGE_READ_DATA_EXPLORER_VIEW, UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW]'
createRoles: '[UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW]'
category: 'visualization'
- shortcutKey: 'e'
+ shortcutKey: 'c'
diff --git a/ui/projects/streampipes/shared-ui/src/lib/services/keyboard-shortcut.service.ts b/ui/projects/streampipes/shared-ui/src/lib/services/keyboard-shortcut.service.ts
index 949732cb63..e50f8adeeb 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/services/keyboard-shortcut.service.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/services/keyboard-shortcut.service.ts
@@ -16,7 +16,7 @@
*
*/
-import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';
+import { Injectable, NgZone, OnDestroy, inject, signal } from '@angular/core';
import { Subject } from 'rxjs';
import { DialogService } from '../dialog/base-dialog/base-dialog.service';
@@ -40,28 +40,63 @@ export type ShortcutRegistration = { unregister: () => void };
@Injectable({ providedIn: 'root' })
export class KeyboardShortcutService implements OnDestroy {
+ readonly shortcutHintsVisible = signal(false);
+
private keydown$ = new Subject<KeyboardEvent>();
private registrations: Map<string, ShortcutAction[]> = new Map();
private sequenceRegistrations: Map<string, SequenceAction[]> = new Map();
private pendingSequenceKey: string | null = null;
private pendingSequenceTimeout: any = null;
+ private shortcutHintTimeout: ReturnType<typeof setTimeout> | null = null;
private readonly SEQUENCE_TIMEOUT_MS = 1000;
+ private readonly SHORTCUT_HINT_DELAY_MS = 200;
private listener = (e: KeyboardEvent) => {
const isInput = this.isInputFocused(e);
const ctrl = e.ctrlKey || e.metaKey;
+ if (e.key === 'Shift') {
+ this.handleShiftKeyDown(e, isInput);
+ }
+
if (!isInput || ctrl || e.key === 'Escape') {
this.keydown$.next(e);
}
};
+ private keyupListener = (e: KeyboardEvent) => {
+ if (e.key === 'Shift') {
+ this.hideShortcutHints();
+ }
+ };
+
+ private windowBlurListener = () => this.hideShortcutHints();
+
+ private focusInListener = (event: FocusEvent) => {
+ if (this.isInputFocused(event)) {
+ this.hideShortcutHints();
+ }
+ };
+
+ private visibilityChangeListener = () => {
+ if (document.hidden) {
+ this.hideShortcutHints();
+ }
+ };
+
private dialogService = inject(DialogService);
constructor(private ngZone: NgZone) {
- this.ngZone.runOutsideAngular(() =>
- document.addEventListener('keydown', this.listener, true),
- );
+ this.ngZone.runOutsideAngular(() => {
+ document.addEventListener('keydown', this.listener, true);
+ document.addEventListener('keyup', this.keyupListener, true);
+ document.addEventListener('focusin', this.focusInListener, true);
+ window.addEventListener('blur', this.windowBlurListener);
+ document.addEventListener(
+ 'visibilitychange',
+ this.visibilityChangeListener,
+ );
+ });
this.keydown$.subscribe(e =>
this.ngZone.run(() => this.handleEvent(e)),
);
@@ -69,8 +104,16 @@ export class KeyboardShortcutService implements OnDestroy {
ngOnDestroy(): void {
document.removeEventListener('keydown', this.listener, true);
+ document.removeEventListener('keyup', this.keyupListener, true);
+ document.removeEventListener('focusin', this.focusInListener, true);
+ window.removeEventListener('blur', this.windowBlurListener);
+ document.removeEventListener(
+ 'visibilitychange',
+ this.visibilityChangeListener,
+ );
this.keydown$.complete();
this.clearPendingSequence();
+ this.hideShortcutHints();
}
register(id: string, actions: ShortcutAction[]): ShortcutRegistration {
@@ -155,11 +198,46 @@ export class KeyboardShortcutService implements OnDestroy {
event.stopPropagation();
}
if (!this.dialogService.hasOpenDialogs || match.allowInDialog) {
+ if (match.shift) {
+ this.hideShortcutHints();
+ }
match.action(event);
}
}
}
+ private handleShiftKeyDown(event: KeyboardEvent, isInput: boolean): void {
+ if (
+ event.repeat ||
+ isInput ||
+ event.ctrlKey ||
+ event.metaKey ||
+ event.altKey ||
+ this.dialogService.hasOpenDialogs ||
+ this.shortcutHintTimeout
+ ) {
+ return;
+ }
+
+ this.shortcutHintTimeout = setTimeout(() => {
+ if (
+ !this.dialogService.hasOpenDialogs &&
+ !this.isEditableElement(document.activeElement)
+ ) {
+ this.ngZone.run(() => this.shortcutHintsVisible.set(true));
+ }
+ this.shortcutHintTimeout = null;
+ }, this.SHORTCUT_HINT_DELAY_MS);
+ }
+
+ private hideShortcutHints(): void {
+ if (this.shortcutHintTimeout) {
+ clearTimeout(this.shortcutHintTimeout);
+ }
+ this.shortcutHintTimeout = null;
+ this.shortcutHintsVisible.set(false);
+ }
+
private clearPendingSequence(): void {
if (this.pendingSequenceTimeout) {
clearTimeout(this.pendingSequenceTimeout);
@@ -168,11 +246,15 @@ export class KeyboardShortcutService implements OnDestroy {
this.pendingSequenceTimeout = null;
}
- private isInputFocused = (event: KeyboardEvent): boolean => {
- const tag = (event.target as HTMLElement)?.tagName;
+ private isInputFocused = (event: Event): boolean =>
+ this.isEditableElement(event.target);
+
+ private isEditableElement(target: EventTarget | null): boolean {
+ const element = target as HTMLElement;
+ const tag = element?.tagName;
return (
['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) ||
- (event.target as HTMLElement)?.isContentEditable
+ element?.isContentEditable
);
- };
+ }
}
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.html b/ui/src/app/core/components/iconbar/iconbar.component.html
index 1b2dbb1cf3..31824fe055 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.html
+++ b/ui/src/app/core/components/iconbar/iconbar.component.html
@@ -64,6 +64,11 @@
[class.hidden]="collapsed()"
>{{ item.title }}</span
>
+ @if (shortcutHintsVisible() && !!item.shortcutKey) {
+ <kbd class="shortcut-key">{{
+ item.shortcutKey | uppercase
+ }}</kbd>
+ }
</button>
}
}
@@ -99,7 +104,11 @@
<div
class="menu-group-items"
[style.maxHeight]="
- group.collapsed && !collapsed() ? '0px' : '500px'
+ group.collapsed &&
+ !collapsed() &&
+ !shortcutHintsVisible()
+ ? '0px'
+ : '500px'
"
[class.instant]="collapsed()"
role="list"
@@ -130,6 +139,14 @@
[class.hidden]="collapsed()"
>{{ item.title | translate }}</span
>
+ @if (
+ shortcutHintsVisible() &&
+ !!item.shortcutKey
+ ) {
+ <kbd class="shortcut-key">{{
+ item.shortcutKey | uppercase
+ }}</kbd>
+ }
</button>
}
}
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.scss b/ui/src/app/core/components/iconbar/iconbar.component.scss
index 8cf254fa5c..3dd8a60688 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.scss
+++ b/ui/src/app/core/components/iconbar/iconbar.component.scss
@@ -209,6 +209,7 @@
/* Menu item button */
.menu-item {
+ position: relative;
width: 100%;
height: var(--mb-item-h);
padding: 8px 10px;
@@ -261,6 +262,32 @@
flex: 1 1 auto;
}
+.shortcut-key {
+ flex: 0 0 auto;
+ min-width: 22px;
+ padding: 2px 5px;
+ color: var(--color-default-text);
+ font-family: inherit;
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
+ line-height: 16px;
+ text-align: center;
+ background: var(--color-bg-0);
+ border: 1px solid var(--color-border-subtle, var(--color-bg-3));
+ border-bottom-width: 2px;
+ border-radius: var(--button-border-radius);
+ box-shadow: 0 2px 6px rgb(0 0 0 / 18%);
+}
+
+.menubar-root.collapsed .shortcut-key {
+ position: absolute;
+ top: 0;
+ right: 1px;
+ min-width: 17px;
+ padding: 0 3px;
+ line-height: 15px;
+}
+
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.ts b/ui/src/app/core/components/iconbar/iconbar.component.ts
index 3ebdeec171..9dfde4a07e 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.ts
+++ b/ui/src/app/core/components/iconbar/iconbar.component.ts
@@ -26,6 +26,7 @@ import {
import { MatIcon } from '@angular/material/icon';
import { MatDivider } from '@angular/material/divider';
import { TranslatePipe } from '@ngx-translate/core';
+import { UpperCasePipe } from '@angular/common';
@Component({
selector: 'sp-iconbar',
@@ -38,6 +39,7 @@ import { TranslatePipe } from '@ngx-translate/core';
FlexDirective,
MatDivider,
TranslatePipe,
+ UpperCasePipe,
],
})
export class IconbarComponent
diff --git a/ui/src/app/core/components/streampipes/streampipes.component.ts b/ui/src/app/core/components/streampipes/streampipes.component.ts
index fa6fea6099..ed80edeece 100644
--- a/ui/src/app/core/components/streampipes/streampipes.component.ts
+++ b/ui/src/app/core/components/streampipes/streampipes.component.ts
@@ -23,7 +23,6 @@ import {
KeyboardShortcutService,
ShortcutRegistration,
ShortcutAction,
- SequenceAction,
} from '@streampipes/shared-ui';
import { TranslateService } from '@ngx-translate/core';
import { CollapseService } from '../../collapse.service';
@@ -34,6 +33,8 @@ import { ClassDirective } from '@ngbracket/ngx-layout/extended';
import { IconbarComponent } from '../iconbar/iconbar.component';
import { ToolbarComponent } from '../toolbar/toolbar.component';
import { Router, RouterOutlet } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+import { ShortcutDialogComponent } from '../../../help/components/shortcuts/shortcut-dialog.component';
@Component({
selector: 'sp-streampipes',
@@ -65,11 +66,11 @@ export class StreampipesComponent implements OnInit, OnDestroy {
private shortcutService = inject(KeyboardShortcutService);
private router = inject(Router);
private navMenuService = inject(NavMenuService);
+ private dialog = inject(MatDialog);
darkMode$: Subscription;
user$: Subscription;
private shortcutReg: ShortcutRegistration;
- private shortcutSeqReg: ShortcutRegistration;
collapsed = this.collapseService.isCollapsed;
@@ -94,7 +95,7 @@ export class StreampipesComponent implements OnInit, OnDestroy {
{
key: '?',
shift: true,
- action: () => this.router.navigateByUrl('help'),
+ action: () => this.openShortcutDialog(),
},
{
key: 'b',
@@ -106,33 +107,37 @@ export class StreampipesComponent implements OnInit, OnDestroy {
alt: true,
action: () => this.collapseService.toggleMenubar(),
},
+ ...this.navMenuService.items
+ .filter(item => !!item.shortcutKey)
+ .map(item => ({
+ key: item.shortcutKey,
+ shift: true,
+ action: () => {
+ if (item.visible !== false) {
+ this.router.navigateByUrl(item.link || '');
+ }
+ },
+ })),
];
- const sequenceActions: SequenceAction[] = this.navMenuService.items
- .filter(item => !!item.shortcutKey)
- .map(item => ({
- sequence: ['g', item.shortcutKey] as [string, string],
- action: () => {
- if (item.visible !== false) {
- this.router.navigateByUrl(item.link || '');
- }
- },
- }));
-
this.shortcutReg = this.shortcutService.register(
'streampipes-global',
singleKeyActions,
);
- this.shortcutSeqReg = this.shortcutService.registerSequences(
- 'streampipes-global',
- sequenceActions,
- );
+ }
+
+ private openShortcutDialog(): void {
+ this.dialog.open(ShortcutDialogComponent, {
+ width: 'min(900px, 90vw)',
+ maxWidth: '90vw',
+ maxHeight: '90vh',
+ autoFocus: false,
+ });
}
ngOnDestroy() {
this.darkMode$?.unsubscribe();
this.user$?.unsubscribe();
this.shortcutReg?.unregister();
- this.shortcutSeqReg?.unregister();
}
}
diff --git a/ui/src/app/help/components/shortcuts/shortcut-dialog.component.html b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.html
new file mode 100644
index 0000000000..728a53f93a
--- /dev/null
+++ b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.html
@@ -0,0 +1,30 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<button
+ mat-icon-button
+ mat-dialog-close
+ class="shortcut-dialog-close"
+ [attr.aria-label]="'Close' | translate"
+>
+ <mat-icon>close</mat-icon>
+</button>
+
+<mat-dialog-content>
+ <sp-shortcuts-tab></sp-shortcuts-tab>
+</mat-dialog-content>
diff --git a/ui/src/app/help/components/shortcuts/shortcut-dialog.component.scss b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.scss
new file mode 100644
index 0000000000..77720637d8
--- /dev/null
+++ b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.scss
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+:host {
+ position: relative;
+ display: block;
+ padding: var(--space-md);
+}
+
+.shortcut-dialog-close {
+ position: absolute;
+ top: var(--space-sm);
+ right: var(--space-sm);
+ z-index: 1;
+}
+
+mat-dialog-content {
+ max-height: 80vh;
+ padding-top: var(--space-lg);
+}
diff --git a/ui/src/app/core/components/iconbar/iconbar.component.ts b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.ts
similarity index 60%
copy from ui/src/app/core/components/iconbar/iconbar.component.ts
copy to ui/src/app/help/components/shortcuts/shortcut-dialog.component.ts
index 3ebdeec171..bab3ea06c4 100644
--- a/ui/src/app/core/components/iconbar/iconbar.component.ts
+++ b/ui/src/app/help/components/shortcuts/shortcut-dialog.component.ts
@@ -16,35 +16,24 @@
*
*/
-import { Component, OnInit } from '@angular/core';
-import { BaseNavigationComponent } from '../base-navigation.component';
-import {
- FlexDirective,
- LayoutAlignDirective,
- LayoutDirective,
-} from '@ngbracket/ngx-layout/flex';
+import { Component } from '@angular/core';
+import { MatIconButton } from '@angular/material/button';
+import { MatDialogClose, MatDialogContent } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
-import { MatDivider } from '@angular/material/divider';
import { TranslatePipe } from '@ngx-translate/core';
+import { ShortcutsTabComponent } from './shortcuts.component';
@Component({
- selector: 'sp-iconbar',
- templateUrl: './iconbar.component.html',
- styleUrls: ['./iconbar.component.scss'],
+ selector: 'sp-shortcut-dialog',
+ templateUrl: './shortcut-dialog.component.html',
+ styleUrl: './shortcut-dialog.component.scss',
imports: [
- LayoutDirective,
- LayoutAlignDirective,
+ MatDialogClose,
+ MatDialogContent,
MatIcon,
- FlexDirective,
- MatDivider,
+ MatIconButton,
+ ShortcutsTabComponent,
TranslatePipe,
],
})
-export class IconbarComponent
- extends BaseNavigationComponent
- implements OnInit
-{
- ngOnInit(): void {
- super.onInit();
- }
-}
+export class ShortcutDialogComponent {}
diff --git a/ui/src/app/help/components/shortcuts/shortcuts.component.ts b/ui/src/app/help/components/shortcuts/shortcuts.component.ts
index fa31bf6538..77e5a41794 100644
--- a/ui/src/app/help/components/shortcuts/shortcuts.component.ts
+++ b/ui/src/app/help/components/shortcuts/shortcuts.component.ts
@@ -125,14 +125,14 @@ export class ShortcutsTabComponent {
!!item.shortcutKey && item.visible !== false,
)
.map(item => ({
- combo: `g then ${item.shortcutKey}`,
- context: 'Anywhere outside inputs',
+ combo: `Shift + ${item.shortcutKey.toUpperCase()}`,
+ context: 'Outside inputs and dialogs',
description: `Navigate to ${item.title}`,
})),
{
- combo: '?',
- context: 'Anywhere outside inputs',
- description: 'Open help page',
+ combo: 'Shift + ?',
+ context: 'Outside inputs and dialogs',
+ description: 'Show all keyboard shortcuts',
},
{
combo: 'Ctrl + B / Alt + B',
diff --git a/ui/src/app/keyboard-shortcut.service.spec.ts b/ui/src/app/keyboard-shortcut.service.spec.ts
index b76d9039bb..2abec4460a 100644
--- a/ui/src/app/keyboard-shortcut.service.spec.ts
+++ b/ui/src/app/keyboard-shortcut.service.spec.ts
@@ -184,4 +184,94 @@ describe('KeyboardShortcutService', () => {
// Cleanup
document.body.removeChild(input);
});
+
+ it('should show shortcut hints after holding Shift', fakeAsync(() => {
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Shift',
+ shiftKey: true,
+ }),
+ );
+
+ tick(199);
+ expect(service.shortcutHintsVisible()).toBe(false);
+
+ tick(1);
+ expect(service.shortcutHintsVisible()).toBe(true);
+
+ document.dispatchEvent(
+ new KeyboardEvent('keyup', {
+ key: 'Shift',
+ }),
+ );
+ expect(service.shortcutHintsVisible()).toBe(false);
+ }));
+
+ it('should not show shortcut hints in editable fields', fakeAsync(() => {
+ let fired = false;
+ service.register('test', [
+ {
+ key: 'p',
+ shift: true,
+ action: () => {
+ fired = true;
+ },
+ },
+ ]);
+
+ const input = document.createElement('input');
+ document.body.appendChild(input);
+ input.focus();
+
+ input.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Shift',
+ shiftKey: true,
+ bubbles: true,
+ }),
+ );
+ input.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'P',
+ shiftKey: true,
+ bubbles: true,
+ }),
+ );
+ tick(200);
+
+ expect(service.shortcutHintsVisible()).toBe(false);
+ expect(fired).toBe(false);
+
+ document.body.removeChild(input);
+ }));
+
+ it('should fire Shift shortcuts before hints appear', fakeAsync(() => {
+ let fired = false;
+ service.register('test', [
+ {
+ key: 'p',
+ shift: true,
+ action: () => {
+ fired = true;
+ },
+ },
+ ]);
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Shift',
+ shiftKey: true,
+ }),
+ );
+ tick(50);
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'P',
+ shiftKey: true,
+ }),
+ );
+
+ expect(fired).toBe(true);
+ expect(service.shortcutHintsVisible()).toBe(false);
+ }));
});