Jump to Table of Contents

The contextmenu Event Fix

Understanding the problem

You want to add a custom context menu to an element, so you add a "contextmenu" event listener to the element. That listener is going to do two basic things:

  1. Prevent the display of the browser's context menu
  2. Position your custom context menu over top of/relative to the target of the event

The code will look something like this:

function onContextmenu(e) {
    e.preventDefault();

    if (!contextmenu) {
      contextmenu = new Y.Overlay({
          bodyContent: "<ul class=\"contextmenu\"><li>Option 1</li><li>Option 2</li><li>Option 3</li></ul>",
          visible: false,
          constrain: true
      });
      contextmenu.render(Y.one("body"));                    
    }

    contextmenu.set("xy", [e.pageX, e.pageY]);
    contextmenu.show();
}

btn.on("contextmenu", onContextmenu);

This code will work great if the "contextmenu" is triggered via the mouse. However, the "contextmenu" event is one of those device-independent events: can be triggered via the mouse, or the keyboard (on Windows using the Menu key, or the Shift + F10 shortcut). When it's triggered via the keyboard you will run into problems. Here's an overview of the obstacles and inconsistencies by browser + platform:

Internet Explorer

  • When the user triggers the "contextmenu" event, the x and y coordinates of the event will be relative to the current position of the mouse cursor. Not useful since the event was fired via the keyboard and the mouse cursor could be anywhere on the screen.
  • When the user presses Shift + F10 IE's menubar will gain focus, with the first item ("File") highlighted. To fix that, you'll need to bind a "keydown" listener for Shift + F10 and call e.preventDefault(). That WILL prevent the menubar from gaining focus, but will also prevent the "contextmenu" event from firing when the user presses Shift + F10.

Firefox on Windows

  • Shift + F10 won't fire the "contextmenu" event, but WILL trigger the display of the browser's context menu.
  • If the "contextmenu" event is triggered via the Menu key, the x and y coordinates will be close to the target's bottom left corner.

Chrome on Windows

  • Both the Menu key and Shift + F10 fire the "contextmenu" event
  • If the "contextmenu" event is triggered via the Menu key, the x and y coordinates will be close to the target's bottom left corner.

Safari, Chrome and Firefox on the Mac

  • No keyboard shortcut available for triggering the "contextmenu" event, unless the screen reader (VoiceOver) is running, in which case the shortcut is Shift + Ctrl + Alt + M.
  • When VoiceOver is running and the user presses Shift + Ctrl + Alt + M, the x and y coordinates will reference the center of the event target.

Opera

  • On Windows, Shift + F10 won't fire the "contextmenu" event, but WILL trigger the display of the browser's context menu.
  • On Mac, Shift + Command + M won't fire "contextmenu" event, but WILL trigger the display of the browser's context menu.

Here's a working example. The following button has a custom context menu. Try to invoke it via the keyboard to see the problems yourself:

The value of the "contextmenu" synthetic event

Returning to the task at hand, as a developer you just want to bind a single "contextmenu" event listener and have it do the right thing regardless of how the event was triggered. This is what the "contextmenu" synthetic event does; it fixes all the aforementioned problems and inconsistencies while maintaining the same signature as a standard "contextmenu" DOM event. Additionally, it provides two bits of sugar:

  1. Prevents the display of the browser's context menu. (Since you're likely going to be doing that anyway.)
  2. Follows Safari's model such that when the "contextmenu" event is fired via the keyboard, the x and y coordinates of the event will reference the center of the target.

All that's required to use the "contextmenu" synthetic event is to add "event-contextmenu" to the use() statement.

YUI().use("event-contextmenu", function (Y) {

});

Here's a working example: The following button has a custom context menu. On Windows the context menu can be invoked by pressing either Menu or using Shift + F10, on the Mac use Shift + Ctrl + Alt + M.

Here's the code for the example:

YUI().use("event-contextmenu", "overlay", function (Y) {

  var btn = Y.one("#btn-2"), 
      contextmenu;

  function onContextmenu(e) {

      if (!contextmenu) {
        contextmenu = new Y.Overlay({
            bodyContent: "<ul class=\"contextmenu\"><li>Option 1</li><li>Option 2</li><li>Option 3</li></ul>",
            visible: false,
            constrain: true
        });
        contextmenu.render(Y.one("body"));                    
      }

      contextmenu.set("xy", [e.pageX, e.pageY]);
      contextmenu.show();
  }

  
  btn.on("contextmenu", onContextmenu);

});