JSPM

@material/select

0.2.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 645173
  • Score
    100M100P100Q167686F
  • License Apache-2.0

The Material Components web select (textfield drop-down) component

Package Exports

  • @material/select
  • @material/select/dist/mdc.select
  • @material/select/dist/mdc.select.css
  • @material/select/dist/mdc.select.js
  • @material/select/foundation
  • @material/select/foundation.js
  • @material/select/index
  • @material/select/index.js
  • @material/select/mdc-select.scss

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@material/select) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

MDC Select

MDC Select provides material design single-option and multi-option select menus. It functions analogously to the browser's native <select> element, and includes a gracefully degraded version that can be used in conjunction with the browser's native element. Both are fully accessible, and fully RTL-aware.

Installation

npm install --save @material/select

Usage

Using the full-fidelity JS component

<div class="mdc-select" role="listbox" tabindex="0">
  <span class="mdc-select__selected-text">Pick a food group</span>
  <div class="mdc-simple-menu mdc-select__menu">
    <ul class="mdc-list mdc-simple-menu__items">
      <li class="mdc-list-item" role="option" id="grains" tabindex="0">
        Bread, Cereal, Rice, and Pasta
      </li>
      <li class="mdc-list-item" role="option" id="vegetables" tabindex="0">
        Vegetables
      </li>
      <li class="mdc-list-item" role="option" id="fruit" tabindex="0">
        Fruit
      </li>
      <li class="mdc-list-item" role="option" id="dairy" tabindex="0">
        Milk, Yogurt, and Cheese
      </li>
      <li class="mdc-list-item" role="option" id="meat" tabindex="0">
        Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts
      </li>
      <li class="mdc-list-item" role="option" id="fats" tabindex="0">
        Fats, Oils, and Sweets
      </li>
    </ul>
  </div>
</div>

Then with JS

import {MDCSelect} from 'mdc-select';

const select = new MDCSelect(document.querySelector('.mdc-select'));
select.listen('MDCSelect:change', () => {
  alert(`Selected "${select.selectedOptions[0].textContent}" at index ${select.selectedIndex}"`);
});

Note that you can include mdc-select via a UMD bundle, which will be available post-alpha.

Note that the full-fidelity version of MDC Select requires you to manually include the styles for mdc-menu and mdc-list. If you are using the material-components-web package, this is taken care of for you and you simply need to import the main stylesheet. Otherwise, you must ensure that you manually include the style dependencies for both the mdc-list and mdc-menu for this component to function properly.

Select with pre-selected option

<div class="mdc-select" role="listbox" tabindex="0">
  <span class="mdc-select__selected-text">Vegetables</span>
  <div class="mdc-simple-menu mdc-select__menu">
    <ul class="mdc-list mdc-simple-menu__items">
      <li class="mdc-list-item" role="option" id="grains" tabindex="0">
        Bread, Cereal, Rice, and Pasta
      </li>
      <li class="mdc-list-item" role="option" aria-selected id="vegetables" tabindex="0">
        Vegetables
      </li>
      <li class="mdc-list-item" role="option" id="fruit" tabindex="0">
        Fruit
      </li>
      <li class="mdc-list-item" role="option" id="dairy" tabindex="0">
        Milk, Yogurt, and Cheese
      </li>
      <li class="mdc-list-item" role="option" id="meat" tabindex="0">
        Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts
      </li>
      <li class="mdc-list-item" role="option" id="fats" tabindex="0">
        Fats, Oils, and Sweets
      </li>
    </ul>
  </div>
</div>

Disabled select

<div class="mdc-select mdc-select--disabled" role="listbox" aria-disabled="true" tabindex="-1">
  <span class="mdc-select__selected-text">Pick a food group</span>
  <div class="mdc-simple-menu mdc-select__menu">
    <ul class="mdc-list mdc-simple-menu__items">
      <li class="mdc-list-item" role="option" id="grains" tabindex="0">
        Bread, Cereal, Rice, and Pasta
      </li>
      <li class="mdc-list-item" role="option" id="vegetables" tabindex="0">
        Vegetables
      </li>
      <li class="mdc-list-item" role="option" id="fruit" tabindex="0">
        Fruit
      </li>
      <li class="mdc-list-item" role="option" id="dairy" tabindex="0">
        Milk, Yogurt, and Cheese
      </li>
      <li class="mdc-list-item" role="option" id="meat" tabindex="0">
        Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts
      </li>
      <li class="mdc-list-item" role="option" id="fats" tabindex="0">
        Fats, Oils, and Sweets
      </li>
    </ul>
  </div>
</div>

Using the Pure CSS Select

The mdc-select CSS class also works on browser's native <select> elements, allowing for a seamless, un-invasive experience in browsers where a native select may be more appropriate, such as on a mobile device. It does not require any javascript, nor any CSS for mdc-menu or mdc-list.

<select class="mdc-select">
  <option value="" default selected>Pick a food</option>
  <option value="grains">Bread, Cereal, Rice, and Pasta</option>
  <option value="vegetables">Vegetables</option>
  <optgroup label="Fruits">
    <option value="apple">Apple</option>
    <option value="oranges">Orange</option>
    <option value="banana">Banana</option>
  </optgroup>
  <option value="dairy">Milk, Yogurt, and Cheese</option>
  <option value="meat">Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts</option>
  <option value="fats">Fats, Oils, and Sweets</option>
</select>

Multi Select

MDC-Web implements multi-select on top of the <select multiple> element.

<select multiple size="6" class="mdc-multi-select mdl-list" >
  <optgroup class="mdc-list-group" label="Starches">
    <option class="mdc-list-item">
      Potato
    </option>
    <option class="mdc-list-item">
      Cereal
    </option>
  </optgroup>
  <option class="mdc-list-divider" role="presentation" disabled />
  <option>
    misc...
  </option>
</select>

Select elements take a size attribute to determine the height of the select box.

If you'd like to maintain the width or height outside of the attribute, you'll need to set it in your styles:

.my-select-container .mdc-select {
  width: 300px;
  height: 550px;
}

Classes

Class Description
mdc-select A pure css select element
mdc-list-group A group of options.
mdc-list-item A list item.
mdc-list-divider A divider.

It is advised that dividers also set role="presentation" to disable selection and not cloud accessibility.

MDC Select Component API

The MDC Select component API is modeled after a subset of the HTMLSelectElement functionality, and is outlined below.

Properties

Property Name Type Description
options HTMLElement[] (read-only) An array of menu items comprising the select's options.
selectedIndex number The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element.
selectedOptions HTMLElement[] (read-only) A NodeList of either the currently selected option, or no elements if nothing is selected.
disabled boolean Whether or not the component is disabled. Settings this sets the disabled state on the component.

Methods

Method Signature Description
item(index: number) => HTMLElement? Analogous to HTMLSelectElement.prototype.item. Returns the option at the specified index, or null if the index is out of bounds. }
nameditem(key: string) => HTMLElement? Analogous to HTMLSelectElement.prototype.nameditem. Returns the options either whose id equals the given key, or whose name attribute equals the given key. Returns null if no item with an id or name attribute of the specified key is found.

Events

The MDC Select JS component emits an MDCSelect:change event when the selected option changes as the result of a user action.

Instantiating using a custom MDCSimpleMenu component.

MDCSelect controls an MDCSimpleMenu instance under the hood in order to display its options. If you'd like to instantiate a custom menu instance, you can provide an optional 3rd menuFactory argument to MDCSelect's constructor.

const menuFactory = menuEl => {
  const menu = new MDCSimpleMenu(menuEl);
  // Do stuff with menu...
  return menu;
};
const selectEl = document.querySelector('.mdc-select');
const select = new MDCSelect(selectEl, /* foundation */ undefined, menuFactory);

The menuFactory function is passed an HTMLElement and is expected to return an MDCSimpleMenu instance attached to that element. This is mostly used for testing purposes, but it's there if you need it nonetheless.

Using the foundation class

MDC Select ships with a foundation class that framework authors can use to integrate MDC Select into their custom components. Note that due to the nature of MDC Select, the adapter is quite complex. We try to provide as much guidance as possible, but we encourage developers to reach out to use via GH Issues or on Gitter if they run into problems.

Notes for component implementors

The MDCSelectFoundation expects that the select component conforms to the following two requirements:

  1. The component owns an element that's used as its select menu, e.g. its menu element.

  2. The component controls an instance of MDCSimpleMenu, which is attached to its menu element.

We achieve this by accepting a menuFactory optional constructor parameter, which is a function which is passed our menu element, and is expected to return an MDCSimpleMenu component instance. If you are attempting to implement mdc-select for your framework, and you find that this approach does not work for you, and there is no suitable way to satisfy the above two requirements, please open an issue.

MDCSelectFoundation also has the ability to resize itself whenever its options change, via the resize() method. We recommend calling this method on initialization, or when the menu items are modified. For example, if building a react component, it may be appropriate to call resize() within componentDidUpdate.

Adapter API

Method Signature Description
addClass(className: string) => void Adds a class to the root element.
removeClass(className: string) => void Removes a class from the root element.
setAttr(attr: string, value: string) => void Sets attribute attr to value value on the root element.
rmAttr(attr: string) => void Removes attribute attr from the root element.
computeBoundingRect() => {left: number, top: number} Returns an object with a shape similar to a ClientRect object, with a left and top property specifying the element's position on the page relative to the viewport. The easiest way to achieve this is by calling getBoundingClientRect() on the root element.
registerInteractionHandler(type: string, handler: EventListener) => void Adds an event listener handler for event type type on the root element.
deregisterInteractionHandler(type: string, handler: EventListener) => void Removes an event listener handler for event type type on the root element.
focus() => void Focuses the root element
makeTabbable() => void Allows the root element to be tab-focused via keyboard. We achieve this by setting the root element's tabIndex property to 0.
makeUntabbable() => void Disallows the root element to be tab-focused via keyboard. We achieve this by setting the root element's tabIndex property to -1.
getComputedStyleValue(propertyName: string) => string Get the root element's computed style value of the given dasherized css property propertyName. We achieve this via getComputedStyle(...).getPropertyValue(propertyName).
setStyle(propertyName: string, value: string) => void Sets a dasherized css property propertyName to the value value on the root element. We achieve this via root.style.setProperty(propertyName, value).
create2dRenderingContext() => {font: string, measureText: (string) => {width: number}} Returns an object which has the shape of a CanvasRenderingContext2d instance. Namely, it has a string property font which is writable, and a method measureText which given a string of text returns an object containing a width property specifying how wide that text should be rendered in the font specified by the font property. An easy way to achieve this is simply document.createElement('canvas').getContext('2d');.
setMenuElStyle(propertyName: string) => void Sets a dasherized css property propertyName to the value value on the menu element.
setMenuElAttr(attr: string, value: string) => void Sets attribute attr to value value on the menu element.
rmMenuElAttr(attr: string) => void Removes attribute attr from the menu element.
getMenuElOffsetHeight() => number Returns the offsetHeight of the menu element.
getOffsetTopForOptionAtIndex(index: number) => number Returns the offsetTop of the option element at the specified index. The index is guaranteed to be in bounds.
openMenu(focusIndex: string) => void Opens the select's menu with focus on the option at the given focusIndex. The focusIndex is guaranteed to be in bounds.
isMenuOpen() => boolean Returns true if the menu is open, false otherwise.
setSelectedTextContent(selectedTextContent: string) => void Sets the text content of the .mdc-select__selected-text element to selectedTextContent.
getNumberOfOptions() => number Returns the number of options contained in the select's menu.
getTextForOptionAtIndex(index: number) => string Returns the text content for the option at the specified index within the select's menu.
setAttrForOptionAtIndex(index: number, attr: string, value: string) => void Sets an attribute attr to value value for the option at the specified index within the select's menu.
rmAttrForOptionAtIndex(index: number, attr: string) => void Removes an attribute attr for the option at the specified index within the select's menu.
registerMenuInteractionHandler(type: string, handler: EventListener) => void Registers an event listener on the menu component's root element. Note that we will always listen for MDCSimpleMenu:selected for change events, and MDCSimpleMenu:cancel to know that we need to close the menu. If you are using a different events system, you could check the event type for either one of these strings and take the necessary steps to wire it up.
deregisterMenuInteractionHandler(type: string, handler: EventListener) => void Opposite of registerMenuInteractionHandler.
getWindowInnerHeight() => number Returns the innerHeight property of the window element.

The full foundation API

MDCSelectFoundation.getSelectedIndex() => number

Returns the index of the currently selected option. Returns -1 if no option is currently selected.

MDCSelectFoundation.setSelectedIndex(selectedIndex: number) => void

Sets the selected index of the component.

MDCSelectFoundation.isDisabled() => boolean

Returns whether or not the select is disabled.

MDCSelectFoundation.setDisabled(disabled: boolean) => void

Enables/disables the select.

Theming

The select's bottom border is set to the current theme's primary color when focused. The select is fully dark theme aware.

Tips / Tricks

Switching between selects for better cross-device UX

Selects are a tricky beast on the web. Many times, a custom select component will work well on large devices with mouse/keyboard capability, but fail miserably on smaller-scale devices without fine-grained pointer capability, such as a phone. Because mdc-select works on native selects, you can easily switch between a custom select on larger devices and a native element on smaller ones.

First, wrap both a custom select and a native select within a wrapper element, let's call it the select-manager.

<div class="select-manager">
  <!-- Custom MDC Select, shown on desktop -->
  <div class="mdc-select" role="listbox" tabindex="0">
    <span class="mdc-select__selected-text">Pick one</span>
    <div class="mdc-simple-menu mdc-select__menu">
      <ul class="mdc-list mdc-simple-menu__items">
        <li class="mdc-list-item" role="option" id="a" tabindex="0">A</li>
        <li class="mdc-list-item" role="option" id="b" tabindex="0">B</li>
        <li class="mdc-list-item" role="option" id="c" tabindex="0">C</li>
      </ul>
    </div>
  </div>
  <!-- Native element, shown on mobile devices -->
  <select class="mdc-select">
    <option value="" selected disabled>Pick one</option>
    <option value="a">A</option>
    <option value="b">B</option>
    <option value="c">C</option>
  </select>
</div>

Then, write some CSS that implements a media query checking for a small screen as well as course pointer interaction. This will ensure that the custom select will still be present on smaller devices that do have mouse/keyboard capability, such as a hybrid tablet or a small browser window on a desktop.

.select-manager > select.mdc-select {
  display: none;
}

@media (max-width: 600px) and (pointer: coarse) {
  .select-manager > .mdc-select[role="listbox"] {
    display: none;
  }

  .select-manager > select.mdc-select {
    display: block;
  }
}

Finally, we need to be able to react to events and keep each component in sync. We can do this in a few lines of JS, and check where the event came from by looking at its type. If it came from the custom component, the type will be MDCSelect:change, otherwise it will simply be change.

const selectManager = document.querySelector('.select-manager');
const selects = {
  custom: MDCSelect.attachTo(selectManager.querySelector('.mdc-select[role="listbox"]')),
  native: MDCSelect.attachTo(selectManager.querySelector('select.mdc-select'))
};
const changeHandler = ({type}) =>  {
  let changedSelect, selectToUpdate, value;
  if (type === 'MDCSelect:change') {
    changedSelect = selects.custom;
    selectToUpdate = selects.native;
    value = changedSelect.selectedOptions[0].id;
  } else {
    changeSelect = selects.native;
    selectToUpdate = selects.custom;
    value = changedSelect.selectedOptions[0].value;
  }
  selectToUpdate.selectedIndex = changedSelect.selectedIndex;
  console.info('Selected value', value);
};
selects.custom.listen('MDCSelect:change', changeHandler);
selects.native.addEventListener('change', changeHandler);

We are looking into building this functionality into MDCSelect in the future.