Odo Dropdown

Custom dropdown component that defaults to native select elements on touch devices.

Support

IE9+ with polyfills.

To support IE<=11, you will need to add classList and Element#closest polyfills.

Dependencies

Odo Base Component, Odo Helpers.

Basic Odo Dropdown

On non-touch devices, this component acts similarly to native select elements. All keyboard events that interact with native select elements may be used with the custom markup.

Markup

<div id="demo-1" class="odo-dropdown">
  <select class="odo-dropdown__select">
    <option value="0">Sunday</option>
    <option value="1">Monday</option>
    <option value="2">Tuesday</option>
    <option value="3">Wednesday</option>
    <option value="4">Thursday</option>
    <option value="5">Friday</option>
    <option value="6">Saturday</option>
  </select>
  <button type="button" class="odo-dropdown__button">
    <div class="odo-dropdown__button-inner">
      <span class="odo-dropdown__default">Sunday</span>
      <span class="odo-dropdown__value"></span>
      <svg class="odo-dropdown__button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
        <path d="M8,12c-0.3,0-0.5-0.1-0.7-0.3l-5-5c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0L8,9.6l4.3-4.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-5,5  C8.5,11.9,8.3,12,8,12z"/>
      </svg>
    </div>
  </button>
</div>

Setup

var dropdown = new OdoDropdown(document.getElementById('demo-1'));

Options Height

The options list has a static max-height applied to it. Feel free to change this, or subclass the dropdown and calculate the height based on the available viewport.

Public Event

The selected day value above is:

The CHANGE event is emitted when a user has selected a value in the dropdown. Use on to listen for the event. OdoDropdown is an event emitter, so you can also use once and off.

dropdown.on(OdoDropdown.EventType.CHANGE, (data) => {
  console.log(data.value);
});

Public Properties

dropdown.value // get or set the value of the select.
dropdown.selectedText // get the selected option's text content.
dropdown.select // get the <select> element.
dropdown.button // get the <button> element.
dropdown.selectedIndex // get or set the value of the <select> by index.
dropdown.disabled // get or set the disabled state of the component.

Public Methods

dropdown.toggleOptionByValue(value, isDisabled)
dropdown.disableOptionByValue('bar')
dropdown.enableOptionByValue('today')
dropdown.getCustomOptions()
dropdown.getNativeOptions()
dropdown.dispose()

Options Placement

Additionally, you can modify the position of the drop down to display the custom options where you would like. In this example, the options are placed below the button.

This example also shows how you can use a <label> with the dropdown. You need the for attribute! You could also put the <label> inside the <button> if you wanted.

CSS

Add a style which moves the options container, then add the class to the main element.

.odo-dropdown--options-below .odo-dropdown__options {
  transform: translate(0, 45px);
}

JavaScript

var dropdown = new OdoDropdown(document.getElementById('demo-2'));

Selected Option

It can be initialized with an option already selected via the selected attribute on an <option>.

Disabled Option(s)

It can be initialized with an option disabled via the disabled attribute on an <option>.

Custom Styling

Instead of importing the css file for the dropdown and overriding what you need, it is recommended to copy the styles to your own stylesheet and modify them there.

Accessibility with WAI-ARIA

This component manages aria-* attributes to provide screen readers with the appropriate information to control and choose options.

If your odo-dropdown__button is not a <button> element, you need to add the role="button" attribute to it.

Usage with React

Use the insertMarkup option and set it to false.

This allows you to create the markup yourself (don't forget aria attributes and roles!) and have options update when your data changes.

You will still need to use the lifecycle methods like componentDidMount to initialize the dropdown instance and componentWillUnmount to dispose of it.

Here's an example render method to give you an idea from a component called LabelledDropdown.

render() {
  const selectedOption = this.props.options.find(option => option.selected) || {};

  return (
    <div className="labelled-dropdown">
      <label htmlFor={this.props.id}>{this.props.label}</label>
      <div className="odo-dropdown" ref={element => this.element = element}>
        <div className={OdoDropdown.Classes.OPTIONS_CONTAINER} role="menu" aria-hidden="true">
          {this.props.options.map((option) => {
            const selected = option.selected ? ' ' + OdoDropdown.Classes.OPTION_SELECTED : '';
            const disabled = option.disabled ? ' ' + OdoDropdown.Classes.OPTION_DISABLED : '';
            const className = OdoDropdown.Classes.OPTION + selected + disabled;
            return <div key={option.value} className={className} data-value={option.value} tabIndex="-1" role="menuitem">{option.label}</div>
          })}
        </div>
        <select className="odo-dropdown__select" name={this.props.name} id={this.props.id} defaultValue={selectedOption.value}>
          {this.props.options.map((option) => (
            <option key={option.value} value={option.value}>{option.label}</option>
          ))}
        </select>
        <button type="button" className="odo-dropdown__button">
          <div className="odo-dropdown__button-inner">
            <span className="odo-dropdown__default">{selectedOption.label}</span>
            <span className="odo-dropdown__value"></span>
            <svg className="odo-dropdown__button-icon" viewBox="0 0 12.021 6.717">
              <polygon points="6.01 6.717 0 0.707 0.707 0 6.01 5.303 11.313 0 12.021 0.707 6.01 6.717"/>
            </svg>
          </div>
        </button>
      </div>
    </div>
  );
}

The LabelledDropdown component could then be used like this:

<LabelledDropdown options={[{ selected: false, value: 'first_name', label: 'First Name' }, { selected: true, value: 'last_name', label: 'Last Name' }]}
  name="sorter" id="sorter-dropdown" label="Sort" />