javascriptkeyboard-eventsmodifier

How to reasonable detect key press without modifier key?


What is the most sensible way to detect if a key (e.g. F) is pressed without any modifier key (e.g. Ctrl, Alt, Alt Gr)?

Do you have to explicitly consider every single modifier key?

KeyboardEvent: ctrlKey property, altKey property, AltGraph key value

window.addEventListener("keydown", (event) => {
  if (!event.ctrlKey && !event.altKey && event.key !== "AltGraph" && event.key === "f") {
    doSomething();
  }
});

KeyboardEvent: getModifierState() method

window.addEventListener("keydown", (event) => {
  if (!event.getModifierState("Control") && !event.getModifierState("Alt") && !event.getModifierState("AltGraph") && event.key === "f") {
    doSomething();
  }
});

Solution

  • Do you have to explicitly consider every single modifier key?

    Unfortunately, yes.

    Why?

    The DOM UI Events specification describes (🔗, 📌) that modifier state must be stored independent of keydown events:

    3.7.3.1.1. User Agent-Level State

    The UA must maintain the following values that are shared for the entire User Agent.

    A key modifier state (initially empty) that keeps track of the current state of each modifier key available on the system.

    …but this state representation is not exposed to the JavaScript runtime. So that leaves you with the responsibility to iterate every modifier key value that matters to your program's code.

    Getting a list of modifier keys

    How can you get a list of these modifier key values? Unfortunately — there's also no JavaScript API that exposes a valid enum list for iteration. 😕

    However, a modifier key table is included (🔗, 📌) in the UI Events key specification, and — at the time I write this answer — the values include:

    There's also a separate table which lists legacy modifier key values:

    Toward copying the list values into your program's source code:

    You might find it easier to scrape this list by running a JavaScript snippet in your console on the spec page — instead of manually copying and pasting the each value:

    const selector = "#key-table-modifier td.key-table-key > code";
    // Or include the legacy modifiers:
    // const selector = ":where(#key-table-modifier, #key-table-modifier-legacy) td.key-table-key > code";
    
    const modifierKeys = [...document.querySelectorAll(selector)].map((element) =>
      JSON.parse(element.textContent.trim()),
    );
    
    console.log(JSON.stringify(modifierKeys)); //=> ["Alt","AltGraph","CapsLock","Control","Fn","FnLock","Meta","NumLock","ScrollLock","Shift","Symbol","SymbolLock"]
    

    Detecting (the absence of) modifier keys

    As noted in your question, you can use the KeyboardEvent: getModifierState() method (spec (🔗, 📌)) to check each modifier key. Here's a working example based on the details in your question:

    const modifierKeys = [
      "Shift",
      "Alt",
      "AltGraph",
      "Control",
      "Meta",
      "CapsLock",
      "Fn",
      "FnLock",
      "NumLock",
      "ScrollLock",
      "Symbol",
      "SymbolLock",
    ];
    
    function isModifierKeyPressed(keyboardEvent) {
      return modifierKeys.some((key) => keyboardEvent.getModifierState(key));
    }
    
    window.addEventListener("keydown", (ev) => {
      if (ev.key === "f" && !isModifierKeyPressed(ev)) {
        console.log('"f" pressed without modifier');
      }
    });

    Code in the TypeScript Playground

    Notes about the code above:

    Array.prototype.some() stops iterating immediately after the first truthy value is returned by the callback argument. You can optimize the number of iteration cycles by ordering the more commonly-used modifier keys to the beginning of the array. In the example, I ordered some according to my intuitions, but you can modify these based on data collected from your users.