Skip to content

Contextual Menu13.10.5

A Contextual Menu is a menu to gather options for a context, or when navigational control are truncated.


$ npm i @if-design-system/contextual-menu@13.10.5

Edit this section

Overview

There is currently no documentation for this section yet.
Contact the Design System team for questions.

If you want to contribute, you can also add the documentation yourself!

Edit this section

Usage

Contextual Menu is used for actions related to the current context, or when additional options are available to the user and there is a space constraint.

Positioning

Depending on where the Contextual Menu appears within the UI, the menu may be left or right aligned so the Contextual Menu is clearly visible.

In tables

When used inside a table cell in a table. We use a horizontal icon, filled with BL 1, BLUE. We've also decreased the height and width to fit it better in the table row layout.

Performing actions on 4 item(s)
Name Age Position Office Salary Availability
John Wicker 38 Hitman London 833 000 Available
John Wicker 38 Hitman London 833 000 Available
Expandable 38 Hitman London 833 000 Available
John Wicker 38 Hitman London 83312 000 Available
Edit this section

Behaviours

Interactions

Hover

Hover

When hovered, the menu item is filled with BE 1, DARK BEIGE.

Focus

Focus

3px outline with 1px outline-offset with color BL 1, BLUE Complement, a direct complement color of BL 1, BLUE.

Edit this section

Accessibility

Remember to use the correct aria- and html-attributes for the Contextual Menu trigger.

Use aria-haspopup to inform that the button has a popup.

Use aria-controls to inform which element it controls.

Use aria-expanded to reflect the expanded state.

Use aria-label or aria-labelledby to assign a label.

<button
  class="if contextual-menu-button js-contextual-menu"
  id="overflow-menu-09943-trigger"
  tabindex="0"
  aria-haspopup="true"
  aria-controls="overflow-menu-09943"
  aria-expanded="true"
  aria-label="Menu title"
  type="button"
></button>

When opening and closing the Contextual Menu, remember to update the related attributes.

const openMenu = () => {
  const contextualMenuHolder = contextualMenuTrigger.parentElement;
  const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
  const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');
  updateAllOptions(contextualMenuList.querySelectorAll('li:not(.separator)'));
  contextualMenu.classList.add('is-open');
  contextualMenuList.classList.add('is-open');
  contextualMenuTrigger.setAttribute('aria-expanded', true);
  document.removeEventListener('click', handleClickOutsidecontextualMenu);
  document.addEventListener('click', handleClickOutsidecontextualMenu);
  window.removeEventListener('resize', adjustMenuPlacement);
  window.addEventListener('resize', adjustMenuPlacement);
};
const closeMenu = () => {
  const contextualMenuHolder = contextualMenuTrigger.parentElement;
  const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
  const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');
  resetIndexOfOptions();
  removePreviouslySelectedMenuItem(contextualMenuList);
  contextualMenu.classList.remove('is-open');
  contextualMenuList.classList.remove('is-open');
  contextualMenuTrigger.setAttribute('aria-expanded', false);
  window.removeEventListener('resize', adjustMenuPlacement);
  document.removeEventListener('click', handleClickOutsidecontextualMenu);
};
Edit this section

Anatomy

Contextual Menu
  1. Contextual Menu trigger
  2. Contextual Menu
Edit this section

Specs

Contextual Menu
Edit this section

Implementation

The Contextual Menu is always triggered form an icon button/control.

<div class="if" style="position: relative;">
  <button
    class="if contextual-menu-button js-contextual-menu"
    id="overflow-menu-07-trigger"
    tabindex="0"
    aria-haspopup="true"
    aria-controls="overflow-menu-07"
    aria-expanded="true"
    aria-label="Menu title"
    type="button"
  ></button>
  <nav
    class="if contextual-menu [is-open]"
    tabindex="-1"
    role="menu"
    aria-labelledby="overflow-menu-07-trigger"
    id="overflow-menu-07"
  >
    <ul class="if [is-open]">
      <li class="if">
        <button tabindex="-1" role="menuitem" class="if" href="asdasd" disabled>Adjust dates</button>
      </li></ul>
  </nav>
</div>

JavaScript implementation suggestion

const debounce = function(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this,
      args = arguments;
    var later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
};

const contextualMenuTriggers = document.querySelectorAll('.if.js-contextual-menu');

const removePreviouslySelectedMenuItem = el => {
  const selected = el.querySelectorAll('.is-focused');
  selected.forEach(li => {
    li.classList.remove('is-focused');
    li.setAttribute('aria-selected', false);
  });
};

contextualMenuTriggers.forEach(contextualMenuTrigger => {
  const adjustMenuPlacement = debounce(function() {
    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
    const contextualMenuTriggerRect = contextualMenuTrigger.getBoundingClientRect();
    const contextualMenuHolderRect = contextualMenuHolder.getBoundingClientRect();
    contextualMenu.style.top =
      contextualMenuHolderRect.top - contextualMenuTriggerRect.top + contextualMenuTriggerRect.height + 'px';
  }, 300);

  adjustMenuPlacement();

  const handleClickOutsidecontextualMenu = e => {
    if (e.target == contextualMenuTrigger) return;

    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
    const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');

    if (contextualMenu.classList.contains('is-open')) {
      if (!contextualMenuList.contains(e.target)) {
        closeMenu();
      }
    }
  };
  const closeMenu = () => {
    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
    const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');
    resetIndexOfStringSuggestions();
    removePreviouslySelectedMenuItem(contextualMenuList);
    contextualMenu.classList.remove('is-open');
    contextualMenuList.classList.remove('is-open');
    contextualMenuTrigger.setAttribute('aria-expanded', false);
    window.removeEventListener('resize', adjustMenuPlacement);
    document.removeEventListener('click', handleClickOutsidecontextualMenu);
  };

  const openMenu = () => {
    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');
    const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');
    updateAllStringSuggestions(contextualMenuList.querySelectorAll('li:not(.separator)'));
    contextualMenu.classList.add('is-open');
    contextualMenuList.classList.add('is-open');
    contextualMenuTrigger.setAttribute('aria-expanded', true);
    document.removeEventListener('click', handleClickOutsidecontextualMenu);
    document.addEventListener('click', handleClickOutsidecontextualMenu);
    window.removeEventListener('resize', adjustMenuPlacement);
    window.addEventListener('resize', adjustMenuPlacement);
  };
  const handlecontextualMenuClick = e => {
    const contextualMenuTrigger = e.target;
    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenu = contextualMenuHolder.querySelector('.if.contextual-menu');

    if (contextualMenu.classList.contains('is-open')) {
      closeMenu();
    } else {
      openMenu();
    }
  };

  const updateAllStringSuggestions = nodes => {
    allSuggestions = Array.prototype.slice.call(nodes).filter(node => !node.querySelector('[disabled]'));
  };
  const resetIndexOfStringSuggestions = () => {
    indexOfSuggestions = 0;
  };
  let indexOfSuggestions = 0;
  let allSuggestions = null;

  const handlecontextualMenuKeypress = e => {
    const contextualMenuTrigger = e.target;
    const contextualMenuHolder = contextualMenuTrigger.parentElement;
    const contextualMenuList = contextualMenuHolder.querySelector('.if.contextual-menu > ul.if');

    if (e.key === 'Enter') {
      e.stopPropagation();
      e.preventDefault();

      const selected = contextualMenuList.querySelector('li.is-focused');
      if (contextualMenuList.classList.contains('is-open') && selected) {
        resetIndexOfStringSuggestions();
        closeMenu();
      }
      return false;
    }

    if (contextualMenuList.classList.contains('is-open')) {
      if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
        return false;
      }

      if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
        let nextElement;
        e.preventDefault();
        if (e.key == 'ArrowUp') {
          nextElement = allSuggestions[--indexOfSuggestions];
          if (!nextElement) {
            indexOfSuggestions = allSuggestions.length - 1;
            nextElement = allSuggestions[indexOfSuggestions];
          }

          removePreviouslySelectedMenuItem(contextualMenuList);
          nextElement.classList.add('is-focused');
          nextElement.setAttribute('aria-selected', true);
        } else if (e.key == 'ArrowDown') {
          nextElement = allSuggestions[++indexOfSuggestions];
          if (!nextElement) {
            indexOfSuggestions = 0;
            nextElement = allSuggestions[indexOfSuggestions];
          }
          removePreviouslySelectedMenuItem(contextualMenuList);
          nextElement.classList.add('is-focused');
          nextElement.setAttribute('aria-selected', true);
        }
      }
    }
  };

  contextualMenuTrigger.removeEventListener('click', handlecontextualMenuClick);
  contextualMenuTrigger.removeEventListener('keyup', handlecontextualMenuKeypress);
  contextualMenuTrigger.addEventListener('click', handlecontextualMenuClick);
  contextualMenuTrigger.addEventListener('keyup', handlecontextualMenuKeypress);
});
Edit this section

Contact us