feat: Added command palette for quick actions and switching between items (#2933) [skip e2e]

* wip: command palette

* use code instead of key

* show recent items above commands

* refactor

* fix command

* add placeholder

* Tab/Shift-Tab to switch tabs

* Fix test

* Add menu item to general account menu

* if shortcut_id is available, use that as the id

* make toggle fn more stable

* small naming changes

* fix name

* Close open modals and popovers when opening command palette

* use stable ids + make sure selectedNotesCount only changes when the count actually changes

* display all commands, even ones in recents list
This commit is contained in:
Aman Harwara
2025-09-25 18:36:09 +05:30
committed by GitHub
parent cb92c10625
commit efba7c682d
61 changed files with 1381 additions and 522 deletions

View File

@@ -41,3 +41,4 @@ export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
export const TOGGLE_KEYBOARD_SHORTCUTS_MODAL = createKeyboardCommand('TOGGLE_KEYBOARD_SHORTCUTS_MODAL')
export const TOGGLE_COMMAND_PALETTE = createKeyboardCommand('TOGGLE_COMMAND_PALETTE')

View File

@@ -1,4 +1,4 @@
import { Environment, Platform } from '@standardnotes/snjs'
import { Environment, Platform, UuidGenerator } from '@standardnotes/snjs'
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
import { KeyboardCommand } from './KeyboardCommands'
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
@@ -28,6 +28,20 @@ export class KeyboardService {
}
}
private isDisabled = false
/**
* When called, the service will stop triggering command handlers
* on keydown/keyup events. Useful when you need to handle events
* yourself while keeping the rest of behaviours inert.
* Make sure to call {@link enableEventHandling} once done.
*/
public disableEventHandling() {
this.isDisabled = true
}
public enableEventHandling() {
this.isDisabled = false
}
get isMac() {
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
}
@@ -116,6 +130,9 @@ export class KeyboardService {
}
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
if (this.isDisabled) {
return
}
for (const command of this.commandMap.keys()) {
const shortcut = this.commandMap.get(command)
if (!shortcut) {
@@ -243,6 +260,7 @@ export class KeyboardService {
...shortcut,
category: handler.category,
description: handler.description,
id: UuidGenerator.GenerateUuid(),
}
}
@@ -250,11 +268,12 @@ export class KeyboardService {
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
* for example by a library like Lexical.
*/
registerExternalKeyboardShortcutHelpItem(item: KeyboardShortcutHelpItem): () => void {
this.keyboardShortcutHelpItems.add(item)
registerExternalKeyboardShortcutHelpItem(item: Omit<KeyboardShortcutHelpItem, 'id'>): () => void {
const itemWithId = { ...item, id: UuidGenerator.GenerateUuid() }
this.keyboardShortcutHelpItems.add(itemWithId)
return () => {
this.keyboardShortcutHelpItems.delete(item)
this.keyboardShortcutHelpItems.delete(itemWithId)
}
}
@@ -262,7 +281,7 @@ export class KeyboardService {
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
* for example by a library like Lexical.
*/
registerExternalKeyboardShortcutHelpItems(items: KeyboardShortcutHelpItem[]): () => void {
registerExternalKeyboardShortcutHelpItems(items: Omit<KeyboardShortcutHelpItem, 'id'>[]): () => void {
const disposers = items.map((item) => this.registerExternalKeyboardShortcutHelpItem(item))
return () => {

View File

@@ -21,8 +21,9 @@ export type PlatformedKeyboardShortcut = KeyboardShortcut & {
export type KeyboardShortcutCategory = 'General' | 'Notes list' | 'Current note' | 'Super notes' | 'Formatting'
export type KeyboardShortcutHelpItem = Omit<PlatformedKeyboardShortcut, 'command'> & {
export interface KeyboardShortcutHelpItem extends Omit<PlatformedKeyboardShortcut, 'command'> {
command?: KeyboardCommand
category: KeyboardShortcutCategory
description: string
id: string
}

View File

@@ -31,6 +31,7 @@ import {
CHANGE_EDITOR_WIDTH_COMMAND,
SUPER_TOGGLE_TOOLBAR,
TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
TOGGLE_COMMAND_PALETTE,
} from './KeyboardCommands'
import { KeyboardKey } from './KeyboardKey'
import { KeyboardModifier, getPrimaryModifier } from './KeyboardModifier'
@@ -108,7 +109,7 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
},
{
command: CHANGE_EDITOR_COMMAND,
key: '/',
key: '?',
modifiers: [primaryModifier, KeyboardModifier.Shift],
preventDefault: true,
},
@@ -200,5 +201,10 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
key: '/',
modifiers: [primaryModifier],
},
{
command: TOGGLE_COMMAND_PALETTE,
code: 'Semicolon',
modifiers: [primaryModifier, KeyboardModifier.Shift],
},
]
}

View File

@@ -1,4 +1,4 @@
export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
export function keyboardCharacterForKeyOrCode(keyOrCode: string, shiftKey = false) {
if (keyOrCode.startsWith('Digit')) {
return keyOrCode.replace('Digit', '')
}
@@ -14,6 +14,8 @@ export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
return '←'
case 'ArrowRight':
return '→'
case 'Semicolon':
return shiftKey ? ':' : ';'
default:
return keyOrCode
}