Menu Button

Menu button     Menu button

A WAI-ARIA friendly menu button component/widget with roles, attributes and behavior in accordance with the specification given in WAI-ARIA Authoring Practices, section 2.20.

Introduction

A menu button is a button that opens a menu. It is often styled as a typical push button with a downward pointing arrow or triangle to hint that activating the button will display a menu.

A menu is a widget that offers a list of choices to the user, such as a set of actions or functions. A menu is (usually) opened, or made visible, by activating a menu button. When a user activates a choice in a menu, the menu (usually) closes.

In this release a <button>, an <input type="text"> or a <div> (mdl-textfield) can have role="button", and act as the control for a menu.

To include a MDLEXT menu button component:

 1. Code a <button> element; this is the clickable toggle that will show and hide the menu. Include an aria-controls attribute whose value will match the id attribute of the unordered list coded in the next step. Inside the button, code a <span> element to hold the button caption text and a <i> element to contain a state icon.

<button aria-controls="menu1">
  <span></span>
  <i></i>
</button>

 2. Code a <ul> unordered list element; this is the container that holds the menu choices. Include an id attribute whose value will match the aria-controls attribute of the button element.

<ul id="menu1">
</ul>

 3. Inside the unordered list, code one <li> element for each choice. Add a text caption as appropriate.

<ul id="menu1">
  <li>Small</li>
  <li>Medium</li>
  <li>Large</li>
</ul>

 4. Add one or more MDL classes, separated by spaces, to the button, span and i elements using the class attribute.

<button class="mdl-button mdl-js-button" aria-controls="menu1">
  <span>Select a value</span>
  <i class="material-icons">more_vert</i>
</button>

 5. Add the mdlext-js-menu-button class to define the element as a menu button component.

<button class="mdl-button mdl-js-button mdlext-js-menu-button" aria-controls="menu1">
  <span>Select a value</span>
  <i class="material-icons">more_vert</i>
</button>

 6. Add the mdlext-menu class to the unordered list and the mdlext-menu__item class to the list items.

<ul id="menu1" class="mdlext-menu">
  <li class="mdlext-menu__item">Small</li>
  <li class="mdlext-menu__item">Medium</li>
  <li class="mdlext-menu__item">Large</li>
</ul>

The menu button component is ready for use.

Note: After page load, the component will add all required Aria roles and attributes.

<button class="mdl-button mdl-js-button mdlext-js-menu-button" 
  role="button" aria-expanded="false" aria-haspopup="true" aria-controls="menu1">
  <span>Select a value</span>
  <i class="material-icons">more_vert</i>
</button>
<ul  id="menu1" class="mdlext-menu" role="menu" tabindex="-1" hidden>
  <li class="mdlext-menu__item" role="menuitem" tabindex="-1">Small</li>
  <li class="mdlext-menu__item" role="menuitem" tabindex="-1">Medium</li>
  <li class="mdlext-menu__item" role="menuitem" tabindex="-1">Large</li>
</ul>

Examples

A menu button with three options.

<button class="mdl-button mdl-js-button mdlext-js-menu-button" aria-controls="menu1">
  <span>Select a value</span>
  <i class="material-icons">more_vert</i>
</button>
<ul id="menu1" class="mdlext-menu">
  <li class="mdlext-menu__item">Small</li>
  <li class="mdlext-menu__item">Medium</li>
  <li class="mdlext-menu__item">Large</li>
</ul>

Note: If the button and the menu has a common ancestor element, the aria-controls attribute of the button and the id attribute of the menu may be skipped.

<div role="presentation">
  <button class="mdl-button mdl-js-button mdlext-js-menu-button">
    <span>Select a value</span>
    <i class="material-icons">more_vert</i>
  </button>
  <ul class="mdlext-menu">
    <li class="mdlext-menu__item">Small</li>
    <li class="mdlext-menu__item">Medium</li>
    <li class="mdlext-menu__item">Large</li>
  </ul>
</div>

With this markup the component will generate a random id attribute for the menu and associate it with the aria-controls of the button.

A menu button with a select listener. Uses a data-value attribute to pass the actual value.

<div>
  <button id="my-button" class="mdl-button mdl-js-button mdl-button--icon mdl-button--primary mdlext-js-menu-button"
    onmenuselect="document.querySelector('#selection').innerHTML = 'Selected value: ' + event.detail.source.getAttribute('data-value');">
    <span>Select</span>
    <i class="mdlext-aria-expanded-more-less"></i>
  </button>
  <ul class="mdlext-menu" hidden >
    <li class="mdlext-menu__item" data-value="10">Ten</li>
    <li class="mdlext-menu__item" data-value="25">Twentyfive</li>
    <li class="mdlext-menu__item" data-value="50">Fifty</li>
  </ul>
</div>
document.querySelector('#my-button').addEventListener('menuselect', function(event) {
  this.querySelector('span').innerHTML = 'Selected value: ' + 
    event.detail.source.getAttribute('data-value')
});

A menu button decorated with icons

<style>
.material-icons.md-18 {
   font-size: 18px;
}
</style>

<button class="mdl-button mdl-button--raised mdl-js-ripple-effect mdl-js-button mdlext-js-menu-button"
  aria-controls="demo-menu">
  <i class="material-icons">gesture</i>
  <span class="mdlext-menu-button__caption">Select</span>
  <i class="material-icons mdlext-aria-expanded-more-less"></i>
</button>

<ul id="demo-menu" class="mdlext-menu" hidden>
  <li class="mdlext-menu__item">
    <i class="material-icons md-18">info</i>
    <span class="mdlext-menu__item__caption">Menu item #1</span>
  </li>
  <li class="mdlext-menu__item">
    <i class="material-icons md-18">help_outline</i>
    <span class="mdlext-menu__item__caption">Menu item #2. A long text to check ellipsis overflow 0123456789</span>
    <i class="material-icons md-18">radio</i>
  </li>
  <li class="mdlext-menu__item-separator"></li>
  <li class="mdlext-menu__item" disabled>
    <span class="mdlext-menu__item__caption">Menu item #3, disabled</span>
    <i class="material-icons md-18">accessibility</i>
  </li>
  <li class="mdlext-menu__item-separator"></li>
  <li class="mdlext-menu__item">
    <span class="mdlext-menu__item__caption">Menu item #IV</span>
    <i class="material-icons md-18">build</i>
  </li>
  <li class="mdlext-menu__item">
    <span class="mdlext-menu__item__caption">Menu item #V</span>
    <i class="material-icons md-18">build</i>
  </li>
  <li class="mdlext-menu__item-separator"></li>
  <li class="mdlext-menu__item">
    <span class="mdlext-menu__item__caption">Menu item #VI</span>
    <i class="material-icons md-18">build</i>
  </li>
  <li class="mdlext-menu__item">
    <span class="mdlext-menu__item__caption">Menu item #VII</span>
    <i class="material-icons md-18">build</i>
  </li>
  <li class="mdlext-menu__item">
    Menu item #n
  </li>
</ul>

A mdl-textfield component can be used as a menu button.

<style>
  .mdl-textfield.mdlext-js-menu-button .mdl-textfield__input {
    padding-right: 40px;
  }
  .mdl-textfield__icon {
    width: 32px;
    text-align: left;
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
  }
  .mdl-textfield.is-disabled .mdl-textfield__icon {
    color: rgba(0, 0, 0, 0.26) !important;
  }
  .mdl-textfield.is-invalid .mdl-textfield__icon {
    color: #de3226 !important;
  }
</style>

<div role="presentation">
  <div id="my-textfield" class="mdl-textfield mdl-js-textfield mdlext-js-menu-button">
    <input class="mdl-textfield__input" type="text" readonly>
    <label class="mdl-textfield__label">Sign in with ...</label>
    <i class="material-icons mdl-textfield__icon mdlext-aria-expanded-more-less"></i>
  </div>
  <ul class="mdlext-menu" hidden >
    <li class="mdlext-menu__item" data-key="S">Small</li>
    <li class="mdlext-menu__item" data-key="M">Medium</li>
    <li class="mdlext-menu__item" data-key="L">Large</li>
  </ul>
</div>
document.querySelector('#my-textfield').addEventListener('menuselect', function(event) {
  this.MaterialTextfield.change(event.detail.source.getAttribute('data-key') 
           + ': ' + event.detail.source.querySelector('span').innerHTML);
});

Create your own state icon with SASS. The _mixins.scss has a mixin which can be used to create custom state icons.

@charset "UTF-8";
.my-aria-expanded-state {
  @include mdlext-aria-expanded-toggle($icon: 'arrow_downward', $icon-expanded: 'arrow_upward');
}

Use a custom styled div as a menu button.

<div role="presentation">
  <div id="my-div" class="mdlext-menu-button mdlext-js-menu-button" 
       style="width:300px; height:44px; max-width:100%; border:1px solid green">
       
    <span class="mdlext-menu-button__caption">Select a size ...</span>
    <i class="material-icons my-aria-expanded-state"></i>
  </div>
  <ul class="mdlext-menu" hidden >
    <li class="mdlext-menu__item" data-key="S">Small</li>
    <li class="mdlext-menu__item" data-key="M">Medium</li>
    <li class="mdlext-menu__item" data-key="L">Large</li>
  </ul>
</div>
document.querySelector('#my-div').addEventListener('menuselect', function(event) {
  this.querySelector('span').innerHTML = 
     event.detail.source.getAttribute('data-key') + ': ' + 
     event.detail.source.querySelector('span').innerHTML);
});

Many buttons can share one menu.

<button class="mdl-button mdl-js-button mdlext-js-menu-button" aria-controls="shared-menu">
  <span class="mdlext-menu-button__caption">A button</span>
</button>

<div class="mdl-textfield mdl-js-textfield mdlext-js-menu-button" aria-controls="shared-menu">
  <input class="mdl-textfield__input" type="text" readonly>
  <label class="mdl-textfield__label">A MDL textfield</label>
</div>

<ul id="shared-menu" class="mdlext-menu" hidden>
  <li class="mdlext-menu__item" role="menuitem">Menu item #1</li>
  <li class="mdlext-menu__item" role="menuitem">Menu item #2</li>
  <li class="mdlext-menu__item" role="menuitem">Menu item #n</li>
</ul>

More examples

Characteristics

Keyboard interaction, Menu Button

  • With focus on the button:
    • Space or Enter: opens the menu, sets aria-expanded="true", and place focus on the previously selected menu item - or on the first menu item if no selected menu item.
    • Down Arrow: opens the menu, sets aria-expanded="true", and moves focus to the first menu item.
    • Up Arrow: opens the menu, sets aria-expanded="true", and moves focus to the last menu item.

Mouse interaction, Menu Button

  • With focus on the button:
    • Click: opens the menu, sets aria-expanded="true", and place focus on the previously selected menu item - or on the first menu item if no selected menu item.
    • Click: a second click closes the menu, sets aria-expanded="false" and place focus on button.

Keyboard interaction, Menu

  • With focus on the menu:
    • Space or Enter: sets aria-selected="true" on active menu item, sets aria-expanded="true" on menu button, closes menu and moves focus back to menu button. The button emits a custom select event with a referfence to the selected menu element.
    • Home: move focus to first menu item.
    • End: move focus to last menu item.
    • Up Arrow or Left Arrow: move focus to previous menu item.
    • Down Arrow or Right Arrow: Move focus to next menu item.
    • Esc: Closes the menu, sets aria-expanded="true" on menu button, and move focus back to menu button.

The keyboard behavior after the menu is open are described in more detail in WAI-ARIA Authoring Practices, 2.19 Menu or Menu bar.

Mouse interaction, Menu

  • With focus on the menu:
    • Click: sets aria-selected="true" on active menu item, sets aria-expanded="true" on menu button, closes menu and moves focus back to menu button. The button emits a custom select event with a referfence to the selected menu element.

WAI-ARIA Roles, States, and Properties

The menu button has the following roles, states and properties set by the menu button component.

Menu Button

  • role="button": the element that opens the menu has role button.
  • aria-haspopup: the element with role button has aria-haspopup set to true.
  • aria-controls: identfies the content on the page (e.g. using IDREFs) that this menu button controls.
  • aria-expanded: the element with role button has aria-expanded set to true if the corresponding menu is open, oterwise false.
  • aria-expanded: when a menu item is disabled, aria-disabled is set to true.
  • disabled": indicates that a button is disabled, otherwise not present.

Menu, WAI-ARIA Roles

  • role="menu": identifies the element as a menu widget.
  • hidden: the menu has attrubute hidden if the controlling buttoun has aria-expanded="false", otherwise the attribute is not present.
  • role="menuitem": identifies an element as a menu item widget.
  • role="menuitemcheckbox": (not yet implemented).
  • role="menuitemradion": (not yet implemented).
  • aria-selected: the selected menu item has aria-selected="true", otherwise not present.
  • role="separator": a divider that separates and distinguishes sections of content or groups of menuitems..
  • disabled": indicates that a menu item is disabled, otherwise not present.

The roles, states, and properties needed for the menu are described in more detail in WAI-ARIA Authoring Practices, 2.19 Menu or Menu bar.

Events emitted from the component

The menu button emits a custom menuselect event when a menu item is selected. The event has a detail object with the following structure:

detail: { 
  source: item // The selected menu item 
}

To set up an event listener to receive the select custom event.

document.querySelector('#my-menubutton').addEventListener('menuselect', function(e) {
  console.log('menu item selected:', e.detail.source);
});

Refer to snippets/menu-button.html or the tests for detailed usage.

Public methods

openMenu(position)

Open menu at given position. Position is on of first, last or selected. Default value is first.

  • first focus first menu item
  • last focus last menu item
  • selected focus previously selected menu item
const menuButton = document.querySelector('#my-menu-button');
menuButton.MaterialExtMenuButton.openMenu();

closeMenu()

Closes an open menu. Moves focus to button.

const menuButton = document.querySelector('#my-menu-button');
menuButton.MaterialExtMenuButton.closeMenu();

getMenuElement()

Get the menu element controlled by this button, null if no menu is controlled by this button.

const menuButton = document.querySelector('#my-menu-button');
const menu = menuButton.MaterialExtMenuButton.getMenuElement(); 

getSelectedMenuItem()

Get a selected menu item element, null if no menu item element selected.

const menuButton = document.querySelector('#my-menu-button');
const element = menuButton.MaterialExtMenuButton.getSelectedMenuItem();
console.log('Selected menu item', element);

setSelectedMenuItem(item)

Set a selected menu item element, typically before menu is opened.

const menuButton = document.querySelector('#my-menu-button');
const menu = menuButton.MaterialExtMenuButton.getMenuElement(); 
menuButton.MaterialExtMenuButton.setSelectedMenuItem(menu.children[1]);

Refer to snippets/menu-button.html or the tests for detailed usage.

Configuration options

The MDLEXT CSS classes apply various predefined visual and behavioral enhancements to the menu button. The table below lists the available classes and their effects.

MDLEXT classEffectRemarks
mdlext-menu-buttonBasic styling for a menu buttonOptional on a div element
mdlext-js-menu-buttonAssigns basic MDLEXT behavior to menu button. Identifies the element as a menu button componentRequired on the element that should act as a menu button
mdlext-menu-button__captionHolds the button textOptional on span element inside menu button element - but required if you want to decorate a button with icons. More than one caption can be used to control various aspects of the button text, e.g. font size.
material-iconsDefines span as a material iconRequired on an inline element. Decorates button or menu item with an icon
mdlext-menuDefines an unordered list container as an MDLEXT componentRequired on ul element
mdlext-menu__itemDefines menu optionsRequired on list item elements
mdlext-menu__item__captionHolds the menu textOptional on span element inside list item element - but required if you want to decorate a menu item with icons. More than one caption can be used to control various aspects of the menu text, e.g. font size.
mdlext-menu__item-separatorItems in a menu may be divided into groups by placing an element with a role of separator between groups.Optional; goes on unordered list element
mdlext-aria-expanded-plus-minusState icon. Displays '+' or '-'Optional; goes on button element
mdlext-aria-expanded-more-lessState icon. Displays 'more' or 'less' Material Design iconsOptional; goes on button element

Note: Disabled versions of the menu items are provided, and are invoked with the standard HTML boolean attribute disabled <li class="mdlext-menu__item" disabled>A menu item</li>. This attribute may be added or removed programmatically via scripting.

If you decorate the menu button with icons, wrap the button text inside a span to separate icons and text

<button class="mdl-button mdl-button--raised mdl-js-ripple-effect mdl-js-button mdlext-js-menu-button">
  <i class="material-icons">gesture</i>
  <span class="mdlext-menu-button__caption">Select</span>
  <i class="material-icons mdlext-menu-button__aria-expanded-more-less"></i>
</button>

If you decorate a menu item with icons, wrap the menu item text inside a span to separate icons and text

<ul id="demo-menu" class="mdlext-menu" hidden>
  <li class="mdlext-menu__item">
    <i class="material-icons md-18">help_outline</i>
    <span class="mdlext-menu__item__caption">Menu item</span>
    <i class="material-icons">info</i>
  </li>
</ul>