Keyboard Shortcuts and Layouts in the Browser

Current DOM specification provides 2 event types for handling keyboard actions[1]:

These events match the physical actions of pressing and releasing a key of the keyboard, and have a payload with the result of keymapping[2] by the OS (or any software in between), taking into account current layout, modifier and dead keys.

The result of keymapping is stored in the key property of the event according to the key algorithm[3]. The pressing of a specific key can be caught just by listening for keydown and checking the key prop. e.g.

let shortcutHandler = (event) => {
    // Shortcut: l
    if (event.key === 'l') {
        // Simplest case; Facebook use the same shortcut to like items in the feed.
        console.log('"l" is pressed')
    }
}

document.addEventListener('keydown', shortcutHandler)

Modifiers

Keyboard shortcuts may require the combination of a character key and one or more modifiers[4]. Modifiers like Meta or Ctrl usually don’t have the purpose of altering the unicode produced by a key, so it’s easy to define and catch a shortcut combination using the modifiers props stored in the event payload. e.g.

let shortcutHandler = (event) => {
    // Shortcut: Meta+c
    if (event.key === 'c' && event.metaKey === true) {
        // The famous "Copy" shortcut on macOS
        console.log('"Meta+c" pressed')
    }
}

On the other hand Alt and Shift modifiers may alter the unicode produced by a key, leading to issues in shortcut definition and catching. e.g.

let shortcutHandler = (event) => {
    // Shortcut: Meta+Shift+s
    if (event.key === 's' && event.metaKey === true && event.shiftKey) {
        // The "Save All" shortcut
        console.log('"Meta+Shift+s" pressed')

        // WARNING
        // This won't work becase the "Shift" modifier change the unicode
        // of "S" key from "s" (lowercase) to "S" (uppercase)
    }
}

Normalization

Fixing the previous example is trivial: just replace the lowercase “s” with the uppercase “S” in the shortcut definition. The new definition considers the effect produced from the Shift modifier, making the code work just fine. e.g.

let shortcutHandler = (event) => {
    // Shortcut: Meta+Shift+S
    if (event.key === 'S' && event.metaKey === true && event.shiftKey) {
        // The Normalized "Save All" shortcut
        console.log('"Meta+Shift+S" pressed')

        // This works as expected
    }
}

Let’s call Normalization the action of replacing of the character key in a keyboard shortcut, with the unicode produced from the same character key plus the modifiers in the shortcut.

Layouts

Normalization falls short when considering multiple layouts; in fact it’s layout dependant. e.g.:

This limits the Normalization effectiveness to just the shortcuts that use a character key for which its modified unicode char, according to the shortcut modifiers, is the same across every layout. Let’s call these shortcuts layout-safe. e.g.:

Recap

When definining shortcuts for the browsers and multiple layouts it’s worth considering that:

Workaround Ideas

Catch ‘Em All

Given a shortcut, the idea is to collect its Normalization for every layout and listening for all of them.

let shortcutHandler = (event) => {
    if ((event.key === '2' && event.metaKey === true && event.shiftKey) ||
        (event.key === '"' && event.metaKey === true && event.shiftKey) ||
        (event.key === '@' && event.metaKey === true && event.shiftKey)) {
        // This catches the "Meta+Shift+2" on US, IT, and FR layouts
        console.log('"Meta+Shift+2" pressed')
    }
}

The obvious problem is that every shortcut ends up producing a set of Normalized shortcuts, and if the interesection of these sets is not empty it’s impossible to choose which shortcut was triggered. e.g.

  1. Normalization[US, IT] for Meta+Shift+= is [Meta+Shift++, Meta+Shift+*]
  2. Normalization[US, IT] for Meta+Shift+8 is [Meta+Shift+*, Meta+Shift+(]

Clearly Meta+Shift+* falls into the intersection, making impossible to know if the source is Meta+Shift+= on IT layout or Meta+Shift+8 on US layout.

Just Ask

The application can infere the keyboard layout using language and location of the browser. Then the Normalization function for such layout is used for catching shortcuts unambiguosly.

Recovering Numbers

Shortcuts with number keys plus modifiers are not layout-safe. However, the number keys position on the physical layout should be the same for most of Latin script layouts [6]. Using the code property of keydown event [5], should be possible to check for numeric keys without ambiguity, as the code property returns the keycode before keymapping is applied.

let shortcutHandler = (event) => {
    if (event.code === 'Digit2' && event.metaKey === true && event.shiftKey) {
        // This also catches "Meta+Shift+2" on Latin Script based layouts
        console.log('"Meta+Shift+2" pressed')
    }
}

Further readings

References

  1. Keypress Event Types - DOM 3 Level Specifications, W3C, https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboardevents
  2. Key Mapping, W3C, https://www.w3.org/TR/DOM-Level-3-Events/#key-mapping
  3. key Property of KeyboardEvent, W3C, https://www.w3.org/TR/DOM-Level-3-Events/#key-algorithm
  4. Modifier Key, Wikipedia, https://en.wikipedia.org/wiki/Modifier_key
  5. Key Codes, W3C, https://www.w3.org/TR/DOM-Level-3-Events/#keys-codevalues
  6. QWERTY-based layouts for Latin script, Keyboard Layout, Wikipedia, https://en.wikipedia.org/wiki/Keyboard_layout#QWERTY-based_layouts_for_Latin_script

– 22/05/2017