From 31c8ac47746199d3e33e6c2b1c0b27c00c2391c5 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 9 Apr 2026 13:16:35 +0200 Subject: [PATCH] fix: make ai panel shortcut command-only --- src-tauri/src/menu.rs | 2 +- src/hooks/appKeyboardShortcuts.ts | 6 +++-- src/hooks/useAppKeyboard.test.ts | 38 ++++++++++++++++++++++++++- src/hooks/useAppKeyboard.ts | 4 +-- tests/smoke/ai-panel-shortcut.spec.ts | 5 ++-- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 5509d431..9fb0317f 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -295,7 +295,7 @@ fn build_note_menu(app: &App) -> MenuResult { .build(app)?; let toggle_ai_chat = MenuItemBuilder::new("Toggle AI Panel") .id(VIEW_TOGGLE_AI_CHAT) - .accelerator("CmdOrCtrl+Shift+L") + .accelerator("Cmd+Shift+L") .build(app)?; let toggle_backlinks = MenuItemBuilder::new("Toggle Backlinks") .id(VIEW_TOGGLE_BACKLINKS) diff --git a/src/hooks/appKeyboardShortcuts.ts b/src/hooks/appKeyboardShortcuts.ts index 1d4c05b6..5cfd1543 100644 --- a/src/hooks/appKeyboardShortcuts.ts +++ b/src/hooks/appKeyboardShortcuts.ts @@ -38,8 +38,10 @@ const VIEW_MODE_KEYS: Record = { } function isTextInputFocused(): boolean { - const tag = document.activeElement?.tagName - return tag === 'INPUT' || tag === 'TEXTAREA' + const active = document.activeElement + if (!(active instanceof HTMLElement)) return false + if (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') return true + return active.isContentEditable || active.closest('[contenteditable="true"]') !== null } function isCommandOrCtrlOnly(e: KeyboardEvent): boolean { diff --git a/src/hooks/useAppKeyboard.test.ts b/src/hooks/useAppKeyboard.test.ts index 8cc2b654..125e21c4 100644 --- a/src/hooks/useAppKeyboard.test.ts +++ b/src/hooks/useAppKeyboard.test.ts @@ -3,6 +3,14 @@ import { renderHook } from '@testing-library/react' import { useAppKeyboard } from './useAppKeyboard' function fireKey(key: string, mods: { altKey?: boolean; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean } = {}) { + fireKeyOnTarget(window, key, mods) +} + +function fireKeyOnTarget( + target: EventTarget, + key: string, + mods: { altKey?: boolean; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean } = {}, +) { const event = new KeyboardEvent('keydown', { key, altKey: mods.altKey ?? false, @@ -12,7 +20,7 @@ function fireKey(key: string, mods: { altKey?: boolean; metaKey?: boolean; ctrlK bubbles: true, cancelable: true, }) - window.dispatchEvent(event) + target.dispatchEvent(event) } function makeActions() { @@ -130,6 +138,14 @@ describe('useAppKeyboard', () => { try { fn() } finally { document.body.removeChild(input) } } + function withFocusedContentEditable(fn: (editable: HTMLDivElement) => void) { + const editable = document.createElement('div') + editable.setAttribute('contenteditable', 'true') + document.body.appendChild(editable) + editable.focus() + try { fn(editable) } finally { document.body.removeChild(editable) } + } + it('Cmd+Backspace does not delete note when text input is focused', () => { const actions = makeActions() renderHook(() => useAppKeyboard(actions)) @@ -139,6 +155,15 @@ describe('useAppKeyboard', () => { }) }) + it('Cmd+Backspace does not delete note when contenteditable is focused', () => { + const actions = makeActions() + renderHook(() => useAppKeyboard(actions)) + withFocusedContentEditable((editable) => { + fireKeyOnTarget(editable, 'Backspace', { metaKey: true }) + expect(actions.onDeleteNote).not.toHaveBeenCalled() + }) + }) + it('Cmd+Backspace deletes note when no text input is focused', () => { const actions = makeActions() renderHook(() => useAppKeyboard(actions)) @@ -201,6 +226,17 @@ describe('useAppKeyboard', () => { }) }) + it('Cmd+Shift+L works when editor stops propagation', () => { + const actions = makeActions() + const onToggleAIChat = vi.fn() + renderHook(() => useAppKeyboard({ ...actions, onToggleAIChat })) + withFocusedContentEditable((editable) => { + editable.addEventListener('keydown', (event) => event.stopPropagation()) + fireKeyOnTarget(editable, 'l', { metaKey: true, shiftKey: true }) + expect(onToggleAIChat).toHaveBeenCalled() + }) + }) + it('Ctrl+Shift+L does not trigger toggle AI chat', () => { const actions = makeActions() const onToggleAIChat = vi.fn() diff --git a/src/hooks/useAppKeyboard.ts b/src/hooks/useAppKeyboard.ts index eaf3acfd..9d63621f 100644 --- a/src/hooks/useAppKeyboard.ts +++ b/src/hooks/useAppKeyboard.ts @@ -14,7 +14,7 @@ export function useAppKeyboard(actions: KeyboardActions) { onKeyDown(event) } - window.addEventListener('keydown', handleWindowKeyDown) - return () => window.removeEventListener('keydown', handleWindowKeyDown) + window.addEventListener('keydown', handleWindowKeyDown, true) + return () => window.removeEventListener('keydown', handleWindowKeyDown, true) }, []) } diff --git a/tests/smoke/ai-panel-shortcut.spec.ts b/tests/smoke/ai-panel-shortcut.spec.ts index b8ff09c1..d0c17d33 100644 --- a/tests/smoke/ai-panel-shortcut.spec.ts +++ b/tests/smoke/ai-panel-shortcut.spec.ts @@ -8,9 +8,10 @@ test.describe('AI panel shortcut', () => { await expect(page.locator('[data-testid="note-list-container"]')).toBeVisible({ timeout: 5_000 }) }) - test('Cmd/Ctrl+Shift+L opens the AI panel', async ({ page }) => { + test('Cmd+Shift+L opens the AI panel from the editor', async ({ page }) => { await page.locator('.app__note-list .cursor-pointer').first().click() - await sendShortcut(page, 'L', ['Control', 'Shift']) + await page.locator('.bn-editor').click() + await sendShortcut(page, 'L', ['Meta', 'Shift']) await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 3_000 }) await expect(page.getByTitle('Close AI panel')).toBeVisible() })