Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions frontend/src/lib/components/Shortcuts/shortcutLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { initKeaTests } from '~/test/init'

import { shortcutLogic } from './shortcutLogic'

// The ⌘⌥ matching path branches on isMac(), captured once at module load. Force it so the
// modifier resolves to 'command' regardless of the host platform running the test.
jest.mock('lib/utils/dom', () => ({
...jest.requireActual('lib/utils/dom'),
isMac: () => true,
}))

describe('shortcutLogic', () => {
let logic: ReturnType<typeof shortcutLogic.build>

beforeEach(() => {
initKeaTests(false)
logic = shortcutLogic()
logic.mount()
})

afterEach(() => {
logic.unmount()
})

function register(name: string, keybind: string[][]): jest.Mock {
const callback = jest.fn()
logic.actions.registerShortcut({ name, keybind, intent: name, interaction: 'function', callback })
return callback
}

function press(init: KeyboardEventInit): void {
window.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, ...init }))
}

it('matches ⌘⌥ shortcuts by the layout-aware key, not the physical key position', () => {
const onC = register('c-action', [['command', 'option', 'c']])
const onI = register('i-action', [['command', 'option', 'i']])

// On Dvorak the key labelled "c" sits where QWERTY has "i": event.code is "KeyI"
// but event.key is the true letter "c". Matching must follow the letter, not the position.
press({ key: 'c', code: 'KeyI', metaKey: true, altKey: true })

expect(onC).toHaveBeenCalledTimes(1)
expect(onI).not.toHaveBeenCalled()
})

it('falls back to the physical key only when Option turns event.key into a non-letter glyph', () => {
const onK = register('k-action', [['command', 'option', 'k']])

// macOS US layout: ⌥K produces the glyph "˚" as event.key, so the physical code is the
// only reliable source of the intended letter.
press({ key: '˚', code: 'KeyK', metaKey: true, altKey: true })

expect(onK).toHaveBeenCalledTimes(1)
})
})
9 changes: 7 additions & 2 deletions frontend/src/lib/components/Shortcuts/shortcutLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,14 @@ export const shortcutLogic = kea<shortcutLogicType>([
pressedKeys.push('option')
}

// Handle Alt key combinations - event.key can change with Alt held
// event.key is layout-aware, so it stays correct on non-QWERTY layouts (Dvorak,
// AZERTY, etc.) where the physical event.code position does not match the letter.
// Prefer it. The only reason to consult the physical event.code is the macOS quirk
// where holding Option turns event.key into a special glyph or dead key (e.g. ⌥K
// becomes "˚"); fall back to event.code only in that case, never when event.key
// already gives us a usable letter or digit.
let keyToAdd = event.key.toLowerCase()
if (event.altKey) {
if (event.altKey && !/^[a-z0-9]$/.test(keyToAdd)) {
const codeMatch = event.code.match(/^Key([A-Z])$/)
if (codeMatch) {
keyToAdd = codeMatch[1].toLowerCase()
Expand Down
Loading