Ignore history shortcuts while editing
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
findShortcutCommandIdForEvent,
|
||||
isAppCommandId,
|
||||
isNativeMenuCommandId,
|
||||
recordSuppressedShortcutCommand,
|
||||
resetAppCommandDispatchStateForTests,
|
||||
type AppCommandHandlers,
|
||||
} from './appCommandDispatcher'
|
||||
@@ -280,4 +281,13 @@ describe('appCommandDispatcher', () => {
|
||||
expect(executeAppCommand(APP_COMMAND_IDS.viewToggleAiChat, handlers, 'renderer-keyboard')).toBe(false)
|
||||
expect(handlers.onToggleAIChat).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('suppresses a native-menu history echo after renderer keyboard yields to text editing', () => {
|
||||
const handlers = makeHandlers()
|
||||
|
||||
recordSuppressedShortcutCommand(APP_COMMAND_IDS.viewGoBack)
|
||||
|
||||
expect(executeAppCommand(APP_COMMAND_IDS.viewGoBack, handlers, 'native-menu')).toBe(false)
|
||||
expect(handlers.onGoBack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,6 +25,8 @@ export type AppCommandDispatchSource =
|
||||
| 'native-menu'
|
||||
| 'app-event'
|
||||
|
||||
type SuppressedShortcutSource = Extract<AppCommandDispatchSource, 'renderer-keyboard'>
|
||||
|
||||
export interface AppCommandHandlers {
|
||||
onSetViewMode: (mode: ViewMode) => void
|
||||
onCreateNote: () => void
|
||||
@@ -154,6 +156,14 @@ let lastCommandDispatch:
|
||||
}
|
||||
| null = null
|
||||
|
||||
let lastSuppressedShortcutCommand:
|
||||
| {
|
||||
id: AppCommandId
|
||||
source: SuppressedShortcutSource
|
||||
timestamp: number
|
||||
}
|
||||
| null = null
|
||||
|
||||
function now(): number {
|
||||
return globalThis.performance?.now?.() ?? Date.now()
|
||||
}
|
||||
@@ -175,6 +185,16 @@ function shouldSuppressDuplicateCommand(
|
||||
return currentTimestamp - lastCommandDispatch.timestamp <= SHORTCUT_ECHO_DEDUPE_WINDOW_MS
|
||||
}
|
||||
|
||||
function shouldSuppressShortcutEchoAfterKeyboardYield(
|
||||
id: AppCommandId,
|
||||
source: AppCommandDispatchSource,
|
||||
currentTimestamp: number,
|
||||
): boolean {
|
||||
if (source !== 'native-menu') return false
|
||||
if (!lastSuppressedShortcutCommand || lastSuppressedShortcutCommand.id !== id) return false
|
||||
return currentTimestamp - lastSuppressedShortcutCommand.timestamp <= SHORTCUT_ECHO_DEDUPE_WINDOW_MS
|
||||
}
|
||||
|
||||
function dispatchActiveTabCommand(
|
||||
pathRef: MutableRefObject<string | null>,
|
||||
handler: (path: string) => void,
|
||||
@@ -249,6 +269,9 @@ export function executeAppCommand(
|
||||
source: AppCommandDispatchSource,
|
||||
): boolean {
|
||||
const timestamp = now()
|
||||
if (shouldSuppressShortcutEchoAfterKeyboardYield(id, source, timestamp)) {
|
||||
return false
|
||||
}
|
||||
if (shouldSuppressDuplicateCommand(id, source, timestamp)) {
|
||||
return false
|
||||
}
|
||||
@@ -260,6 +283,14 @@ export function executeAppCommand(
|
||||
return dispatched
|
||||
}
|
||||
|
||||
export function recordSuppressedShortcutCommand(
|
||||
id: AppCommandId,
|
||||
source: SuppressedShortcutSource = 'renderer-keyboard',
|
||||
): void {
|
||||
lastSuppressedShortcutCommand = { id, source, timestamp: now() }
|
||||
}
|
||||
|
||||
export function resetAppCommandDispatchStateForTests(): void {
|
||||
lastCommandDispatch = null
|
||||
lastSuppressedShortcutCommand = null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
APP_COMMAND_IDS,
|
||||
executeAppCommand,
|
||||
findShortcutCommandIdForEvent,
|
||||
recordSuppressedShortcutCommand,
|
||||
type AppCommandHandlers,
|
||||
} from './appCommandDispatcher'
|
||||
|
||||
@@ -33,6 +34,10 @@ export type KeyboardActions = Pick<
|
||||
>
|
||||
|
||||
const TEXT_EDITING_KEYS = new Set(['Backspace', 'Delete'])
|
||||
const TEXT_EDITING_BLOCKED_COMMANDS = new Set([
|
||||
APP_COMMAND_IDS.viewGoBack,
|
||||
APP_COMMAND_IDS.viewGoForward,
|
||||
])
|
||||
|
||||
function isTextInputFocused(): boolean {
|
||||
const active = document.activeElement
|
||||
@@ -44,7 +49,15 @@ function isTextInputFocused(): boolean {
|
||||
export function handleAppKeyboardEvent(actions: KeyboardActions, event: KeyboardEvent) {
|
||||
const commandId = findShortcutCommandIdForEvent(event)
|
||||
if (commandId === null) return
|
||||
if (TEXT_EDITING_KEYS.has(event.key) && isTextInputFocused()) return
|
||||
|
||||
const textInputFocused = isTextInputFocused()
|
||||
if (textInputFocused) {
|
||||
if (TEXT_EDITING_KEYS.has(event.key)) return
|
||||
if (TEXT_EDITING_BLOCKED_COMMANDS.has(commandId)) {
|
||||
recordSuppressedShortcutCommand(commandId, 'renderer-keyboard')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
if (commandId === APP_COMMAND_IDS.editFindInVault) {
|
||||
|
||||
@@ -43,6 +43,8 @@ function makeActions() {
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
onZoomReset: vi.fn(),
|
||||
onGoBack: vi.fn(),
|
||||
onGoForward: vi.fn(),
|
||||
activeTabPathRef: { current: '/vault/test.md' } as React.MutableRefObject<string | null>,
|
||||
multiSelectionCommandRef: { current: null },
|
||||
}
|
||||
@@ -283,6 +285,38 @@ describe('useAppKeyboard', () => {
|
||||
expect(actions.onDeleteNote).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+Left does not go back when contenteditable is focused', () => {
|
||||
const actions = makeActions()
|
||||
renderHook(() => useAppKeyboard(actions))
|
||||
withFocusedContentEditable((editable) => {
|
||||
fireKeyOnTarget(editable, 'ArrowLeft', { metaKey: true, code: 'ArrowLeft' })
|
||||
expect(actions.onGoBack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('Cmd+Right does not go forward when contenteditable is focused', () => {
|
||||
const actions = makeActions()
|
||||
renderHook(() => useAppKeyboard(actions))
|
||||
withFocusedContentEditable((editable) => {
|
||||
fireKeyOnTarget(editable, 'ArrowRight', { metaKey: true, code: 'ArrowRight' })
|
||||
expect(actions.onGoForward).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('Cmd+Left still goes back when no text input is focused', () => {
|
||||
const actions = makeActions()
|
||||
renderHook(() => useAppKeyboard(actions))
|
||||
fireKey('ArrowLeft', { metaKey: true, code: 'ArrowLeft' })
|
||||
expect(actions.onGoBack).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+Right still goes forward when no text input is focused', () => {
|
||||
const actions = makeActions()
|
||||
renderHook(() => useAppKeyboard(actions))
|
||||
fireKey('ArrowRight', { metaKey: true, code: 'ArrowRight' })
|
||||
expect(actions.onGoForward).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Cmd+K still works when text input is focused', () => {
|
||||
const actions = makeActions()
|
||||
renderHook(() => useAppKeyboard(actions))
|
||||
|
||||
Reference in New Issue
Block a user