<div class="dropdown__container" data-dropdown-container>
    <button class="dropdown__btn" data-dropdown-toggle aria-haspopup="true" aria-expanded="false">
      Dropdown 1
    </button>
    <div class="dropdown__panel" data-dropdown-panel>
        <ul class="u-list-unstyled dropdown__list">
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
        </ul>
    </div>
</div>
<div class="dropdown__container" data-dropdown-container>
    <button class="dropdown__btn" data-dropdown-toggle aria-haspopup="true" aria-expanded="false">
      Dropdown 2
    </button>
    <div class="dropdown__panel" data-dropdown-panel>
        <ul class="u-list-unstyled dropdown__list">
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
            <li class="dropdown__item" role="none">
                <a href="#" class="dropdown__link" role="menuitem" tabindex="-1">
              This is an item
            </a>
            </li>
        </ul>
    </div>
</div>
{% for dropdown in dropdowns %}
  <div class="dropdown__container" data-dropdown-container>
    <button class="dropdown__btn" data-dropdown-toggle aria-haspopup="true" aria-expanded="false">
      {{ dropdown.btnText }}
    </button>
    <div class="dropdown__panel" data-dropdown-panel>
      <ul class="u-list-unstyled dropdown__list">
        {% for item in dropdown.list %}
          <li class="dropdown__item" role="none">
            <a href="{{ item.href }}" class="dropdown__link" role="menuitem" tabindex="-1">
              {{ item.text }}
            </a>
          </li>
        {% endfor %}
      </ul>
    </div>
  </div>
{% endfor %}
{
  "dropdowns": [
    {
      "btnText": "Dropdown 1",
      "list": [
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        }
      ]
    },
    {
      "btnText": "Dropdown 2",
      "list": [
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        },
        {
          "href": "#",
          "text": "This is an item"
        }
      ]
    }
  ]
}
  • Content:
    .dropdown {
    
      &__container {
        position: relative;
        display: inline-flex;
    
        &.is-open {
    
          .dropdown__panel {
            top: 45px;
            right: auto;
            left: 0;
          }
        }
      }
    
      &__item {
        margin-top: 0;
        border-top: 1px solid color(neutral, lighter);
        padding: 0;
        background-color: color(default, light);
        cursor: pointer;
        text-align: left;
        font-family: $brand-font;
    
        &:first-child {
          border-top: 0;
        }
    
        &:focus,
        &:hover {
          background-color: color(secondary, light);
        }
      }
    
      &__panel {
        z-index: z-index(dropdown) - 1;
        position: absolute;
        top: auto;
        left: -9999px;
        box-shadow: 0 10px 30px -10px rgba(color(default, dark), .07);
        border: 1px solid color(neutral, lighter);
        min-width: 230px;
        background-color: color(default, light);
      }
    
      &__link {
        display: inline-block;
        padding: 12px 18px 10px;
        width: 100%;
        cursor: pointer;
        text-align: left;
        text-decoration: none;
        font-family: inherit;
        color: color(neutral, dark);
      }
    }
    
  • URL: /components/raw/button-dropdown/_button-dropdown.scss
  • Filesystem Path: components/button-dropdown/_button-dropdown.scss
  • Size: 1.1 KB
  • Content:
    let dropdownContainers = document.querySelectorAll('[data-dropdown-container]');
    
    for (let i = 0; i < dropdownContainers.length; i++) {
    
      let dropdownToggle = dropdownContainers[i].querySelector('[data-dropdown-toggle]');
      let dropdownPanel = dropdownContainers[i].querySelector('[data-dropdown-panel]');
      let listLinks = dropdownContainers[i].querySelectorAll('.dropdown__link');
      let dropdownStatus = false;
    
      // Watch for clicks outside of the panel or toggle
      document.addEventListener("click", (e) => {
        let panelClicked = dropdownPanel.contains(e.target);
        let toggleClicked = dropdownToggle.contains(e.target);
    
        if (dropdownStatus && (!panelClicked && !toggleClicked)) {
          closeDropdown(dropdownToggle, listLinks);
          dropdownStatus = false;
        }
      })
    
      // Watch for direct clicks on toggle
      dropdownToggle.addEventListener("click", () => {
        dropdownStatus = !dropdownStatus;
        handleDropdown(dropdownToggle, listLinks, dropdownStatus);
      });
    
      document.addEventListener("keydown", (e) => {
        if (!dropdownStatus && (e.keyCode == 13)) {
          handleDropdown(dropdownToggle, listLinks, dropdownStatus);
        }
      });
    
      document.addEventListener("keydown", (e) => {
        if (dropdownStatus && (e.keyCode == 27)) {
          closeDropdown(dropdownToggle, listLinks);
          dropdownStatus = false;
        }
      });
    
      for (let i = 0; i < listLinks.length; i++) {
        listLinks[i].addEventListener("keydown", (e) => {
    
          if (e.keyCode === 38) {
            if (i === 0) {
              listLinks[listLinks.length - 1].focus();
            } else {
              listLinks[i - 1].focus();
            }
          }
    
          if (e.keyCode === 40) {
            if (i === listLinks.length - 1) {
              listLinks[0].focus();
            } else {
              listLinks[i + 1].focus();
            }
          }
        });
      };
    };
    
    let handleDropdown = (element, list, status) => {
      let parent = element.parentNode;
    
      if (status) {
        parent.classList.add('is-open');
        element.setAttribute("aria-expanded", "true");
    
        for (let i = 0; i < list.length; i++) {
          list[i].setAttribute("tabindex", "0");
        }
      } else {
        parent.classList.remove('is-open');
        element.setAttribute("aria-expanded", "false");
    
        for (let i = 0; i < list.length; i++) {
          list[i].setAttribute("tabindex", "-1");
        }
      }
    }
    
    let closeDropdown = (element, list) => {
      element.parentNode.classList.remove('is-open');
      element.setAttribute("aria-expanded", "false");
    
      for (let i = 0; i < list.length; i++) {
        list[i].setAttribute("tabindex", "-1");
      }
    }
    
  • URL: /components/raw/button-dropdown/button-dropdown.js
  • Filesystem Path: components/button-dropdown/button-dropdown.js
  • Size: 2.5 KB

Usage

Button dropdowns are used to toggle nested information. If a user clicks outside the element while the dropdown menu is open, the menu will close. If a user activates another dropdown menu while the dropdown menu is open, the active menu will close.

This button dropdown comes with the following configuration attributes:

  • data-dropdown-toggle defines the button or element that will trigger the dropdown.
  • data-dropdown-panel defines the sub panel that contains all of the nested information.
  • In situations where the panel is open and the user clicks off of the element, the panel will close. This is also the case when another dropdown on the same page is opened.

Labelling Expectations

  • Each dropdown toggle button needs aria-haspopup="true".
  • If the dropdown menu is visible, the dropdown toggle button should have aria-expanded set to true. If the dropdown menu is not visible, aria-expanded is set to false.
  • If the dropdown menu is visible, its menu items should have tabindex="0" . If the dropdown menu is not visible, its menu items should have tabindex="-1".
  • Each list item in the dropdown menu should have role="none", and each items inner link should have role=“menu item”

Focus Expectations

  • Button should have visible keyboard focus state
  • Menu items should have visible keyboard focus state
  • All keyboard interactions relate to when the button or menu items are focused

Keyboard Expectations

  • Enter or Space = Expand/Collapse dropdown menu
  • Tab = If dropdown menu open, move to next focusable element
  • Shift + Tab = If dropdown menu open, move to previous focusable element
  • Esc = Collapse dropdown menu

Screen Reader Expectations

When the button dropdown is focused, screen readers should announce the following:

  • Button text
  • Menu items

Tab Order Expectations

When navigating through a button dropdown, the following tab order is expected:

  1. Button
  2. Menu items (if the menu is visible/expanded)
  3. Next focusable element