Keyup event not firing when meta key is held (Mac OSX only?) - How to handle in javascript?

Viewed 44

Seems like others have ran into this issue outside of the browser. I'm trying to figure out how to deal with it in my javascript code.

When I hold command (on mac) and press and let go of another key, (such as if I were to enter command-c to copy some text), the keyup event does not fire unless I let go of command first.

This affects me because my web app responds to the alt-x key combination to toggle a menu, and this is important. I'm using keyup and keydown events to track the state of which keys are pressed at any given time, so that I can toggle the menu when the user enters alt-x (and not some other combination including alt-x). It works great until I enter command-[some key] and then my program thinks I'm entering [some key]-alt-x when I enter alt-x to toggle the menu.

Does anyone have any ideas for how to deal with this situation?

I made a code pen to illustrate the problem.

const pressed_keys = {};

const keydown_handler = e => {
  pressed_keys[e.code] = true;
  if (has_entered_alt_x(e)) {
    document.querySelector('div').classList.toggle('blue');
  }
};

const keyup_handler = e => pressed_keys[e.code] = false;

const has_entered_alt_x = e => {
  if (!(e.altKey && e.code == 'KeyX')) return false;

  for (const key in pressed_keys) {
    if (Object.hasOwnProperty.call(pressed_keys, key)) {
      if (!['AltLeft', 'AltRight', 'KeyX', 'CapsLock'].includes(key) && 
          pressed_keys[key]) {
        return false;
      }                    
    }
  }

  return true;
}

document.addEventListener('DOMContentLoaded', () => { 
  document.addEventListener('keydown', keydown_handler);
  document.addEventListener('keyup', keyup_handler);
});
div {
  width: 40px;
  height: 40px;
  background-color: red;
}

.blue {
  background-color: blue;
}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <div></div>
  </body>
</html>
1 Answers

Here's a decent work around I came up with, and you can try it in this code pen.

const pressed_keys = {};

const keyup_handler = e => { 
  pressed_keys[e.code] = false;
  // In macos the keyup event doesn't fire when the Meta key is held down. 
  // So, when Meta key is lifted, assume other keys have also been lifted 
  // as well. There is a bug introduced: If the user is quick, they can 
  // hit meta-c, lift the meta key and then hit alt-x very quickly 
  // thereafter, and in this case they will be able to toggle the change 
  // while holding down some other key. This is unlikely to occur and in 
  // the event that it does, it doesn't cause problems for the application.
  if (e.key == 'Meta') {
    Object.keys(pressed_keys).forEach(key => { pressed_keys[key] = false; });
  }
}

const keydown_handler = e => {
  // In macos the keyup doesn't fire on the Meta key (command) after 
  // certain key combinations in the browser, such as
  //   M-Q : shutdown computer 
  //   M-p : print page 
  //   M-o : opens files 
  //   M-t : new tab 
  //   M-y : opens history
  // and possibly others. So, to be able to detect when this has happened, 
  // on keydown ask if the meta key is pressed using e.meta and query 
  // pressed_keys to see if the key up event has fired. If meta isn't 
  // pressed by the keyup event hasn't fired set all pressed_keys to false.
  if (!e.metaKey && (pressed_keys['MetaLeft'] || pressed_keys['MetaRight'])) {
    Object.keys(pressed_keys).forEach(key => { pressed_keys[key] = false; });
  }
  
  pressed_keys[e.code] = true;
  if (has_entered_alt_x(e)) {
    document.querySelector('div').classList.toggle('blue');
  }
};

const has_entered_alt_x = e => {
  if (!(e.altKey && e.code == 'KeyX')) return false;

  for (const key in pressed_keys) {
    if (Object.hasOwnProperty.call(pressed_keys, key)) {
      if (!['AltLeft', 'AltRight', 'KeyX', 'CapsLock'].includes(key) && 
          pressed_keys[key]) {
        return false;
      }                    
    }
  }

  return true;
}

document.addEventListener('DOMContentLoaded', () => { 
  document.addEventListener('keydown', keydown_handler);
  document.addEventListener('keyup', keyup_handler);
});
Related