From b4a921f511d4a7efd1be6bb6fee6c9b19d4dfce7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 14 Jan 2019 17:59:41 -0800 Subject: [PATCH] Tab and keyboard navigation for multi-select --- ui/app/components/multi-select-dropdown.js | 60 +++++++++++++++++++ ui/app/styles/components/dropdown.scss | 18 +++++- .../components/multi-select-dropdown.hbs | 10 +++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/ui/app/components/multi-select-dropdown.js b/ui/app/components/multi-select-dropdown.js index 5c03ea413..a995ac195 100644 --- a/ui/app/components/multi-select-dropdown.js +++ b/ui/app/components/multi-select-dropdown.js @@ -1,6 +1,12 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; +const TAB = 9; +const ESC = 27; +const SPACE = 32; +const ARROW_UP = 38; +const ARROW_DOWN = 40; + export default Component.extend({ classNames: ['dropdown'], @@ -9,6 +15,9 @@ export default Component.extend({ onSelect() {}, + isOpen: false, + dropdown: null, + actions: { toggle({ key }) { const newSelection = this.get('selection').slice(); @@ -19,5 +28,56 @@ export default Component.extend({ } this.get('onSelect')(newSelection); }, + + openOnArrowDown(dropdown, e) { + // It's not a good idea to grab a dropdown reference like this, but it's necessary + // in order to invoke dropdown.actions.close in traverseList + this.set('dropdown', dropdown); + + if (!this.get('isOpen') && e.keyCode === ARROW_DOWN) { + dropdown.actions.open(e); + e.preventDefault(); + } else if (this.get('isOpen') && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) { + const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns'); + const firstElement = document.querySelector(`#${optionsId} .dropdown-option`); + + if (firstElement) { + firstElement.focus(); + e.preventDefault(); + } + } + }, + + traverseList(option, e) { + if (e.keyCode === ESC) { + // Close the dropdown + const dropdown = this.get('dropdown'); + if (dropdown) { + dropdown.actions.close(e); + // Return focus to the trigger so tab works as expected + const trigger = this.element.querySelector('.dropdown-trigger'); + if (trigger) trigger.focus(); + e.preventDefault(); + this.set('dropdown', null); + } + } else if (e.keyCode === ARROW_UP) { + // previous item + const prev = e.target.previousElementSibling; + if (prev) { + prev.focus(); + e.preventDefault(); + } + } else if (e.keyCode === ARROW_DOWN) { + // next item + const next = e.target.nextElementSibling; + if (next) { + next.focus(); + e.preventDefault(); + } + } else if (e.keyCode === SPACE) { + this.send('toggle', option); + e.preventDefault(); + } + }, }, }); diff --git a/ui/app/styles/components/dropdown.scss b/ui/app/styles/components/dropdown.scss index e991f3371..6f37e0bf4 100644 --- a/ui/app/styles/components/dropdown.scss +++ b/ui/app/styles/components/dropdown.scss @@ -6,6 +6,12 @@ box-shadow: $button-box-shadow-standard; background: $white-bis; border: 1px solid $grey-light; + outline: none; + cursor: pointer; + + &:focus { + box-shadow: $button-box-shadow-standard, inset 0 0 0 2px $grey-lighter; + } &.is-outlined { border-color: rgba($white, 0.5); @@ -41,6 +47,10 @@ .dropdown-trigger { border-radius: 0; box-shadow: none; + + &:focus { + box-shadow: inset 0 0 0 2px $grey-lighter; + } } .dropdown:first-child { @@ -107,8 +117,14 @@ border-top: 1px solid $grey-lighter; } - &:hover { + &:hover, + &:focus { background: $white-bis; + outline: none; + border-left: 2px solid $blue; + label { + padding-left: 6px; + } } } } diff --git a/ui/app/templates/components/multi-select-dropdown.hbs b/ui/app/templates/components/multi-select-dropdown.hbs index 34d65160b..2aea751b6 100644 --- a/ui/app/templates/components/multi-select-dropdown.hbs +++ b/ui/app/templates/components/multi-select-dropdown.hbs @@ -1,5 +1,8 @@ -{{#basic-dropdown as |dd|}} - {{#dd.trigger class="dropdown-trigger"}} +{{#basic-dropdown + onOpen=(action (mut isOpen) true) + onClose=(action (mut isOpen) false) + as |dd|}} + {{#dd.trigger class="dropdown-trigger" onKeyDown=(action "openOnArrowDown")}} {{label}} {{#if selection.length}} @@ -11,10 +14,11 @@ {{#dd.content class="dropdown-options"}}