X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/07b889547d28e68e5fc8f923c166bd607da17ad7..refs/pull/1626/head:/resources/assets/js/components/dropdown.js diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js index 3887e8432..4de1e239b 100644 --- a/resources/assets/js/components/dropdown.js +++ b/resources/assets/js/components/dropdown.js @@ -1,3 +1,5 @@ +import {onSelect} from "../services/dom"; + /** * Dropdown * Provides some simple logic to create simple dropdown menus. @@ -10,14 +12,16 @@ class DropDown { this.moveMenu = elem.hasAttribute('dropdown-move-menu'); this.toggle = elem.querySelector('[dropdown-toggle]'); this.body = document.body; + this.showing = false; this.setupListeners(); } - show(event) { + show(event = null) { this.hideAll(); this.menu.style.display = 'block'; this.menu.classList.add('anim', 'menuIn'); + this.toggle.setAttribute('aria-expanded', 'true'); if (this.moveMenu) { // Move to body to prevent being trapped within scrollable sections @@ -38,10 +42,17 @@ class DropDown { }); // Focus on first input if existing - let input = this.menu.querySelector('input'); + const input = this.menu.querySelector('input'); if (input !== null) input.focus(); - event.stopPropagation(); + this.showing = true; + + const showEvent = new Event('show'); + this.container.dispatchEvent(showEvent); + + if (event) { + event.stopPropagation(); + } } hideAll() { @@ -53,6 +64,7 @@ class DropDown { hide() { this.menu.style.display = 'none'; this.menu.classList.remove('anim', 'menuIn'); + this.toggle.setAttribute('aria-expanded', 'false'); if (this.moveMenu) { this.menu.style.position = ''; this.menu.style.left = ''; @@ -60,22 +72,78 @@ class DropDown { this.menu.style.width = ''; this.container.appendChild(this.menu); } + this.showing = false; + } + + getFocusable() { + return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])')); + } + + focusNext() { + const focusable = this.getFocusable(); + const currentIndex = focusable.indexOf(document.activeElement); + let newIndex = currentIndex + 1; + if (newIndex >= focusable.length) { + newIndex = 0; + } + + focusable[newIndex].focus(); + } + + focusPrevious() { + const focusable = this.getFocusable(); + const currentIndex = focusable.indexOf(document.activeElement); + let newIndex = currentIndex - 1; + if (newIndex < 0) { + newIndex = focusable.length - 1; + } + + focusable[newIndex].focus(); } setupListeners() { // Hide menu on option click this.container.addEventListener('click', event => { - let possibleChildren = Array.from(this.menu.querySelectorAll('a')); - if (possibleChildren.indexOf(event.target) !== -1) this.hide(); + const possibleChildren = Array.from(this.menu.querySelectorAll('a')); + if (possibleChildren.includes(event.target)) { + this.hide(); + } + }); + + onSelect(this.toggle, event => { + event.stopPropagation(); + this.show(event); + if (event instanceof KeyboardEvent) { + this.focusNext(); + } }); - // Show dropdown on toggle click - this.toggle.addEventListener('click', this.show.bind(this)); - // Hide menu on enter press - this.container.addEventListener('keypress', event => { - if (event.keyCode !== 13) return true; + + // Keyboard navigation + const keyboardNavigation = event => { + if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { + this.focusNext(); + event.preventDefault(); + } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { + this.focusPrevious(); event.preventDefault(); + } else if (event.key === 'Escape') { this.hide(); - return false; + this.toggle.focus(); + event.stopPropagation(); + } + }; + this.container.addEventListener('keydown', keyboardNavigation); + if (this.moveMenu) { + this.menu.addEventListener('keydown', keyboardNavigation); + } + + // Hide menu on enter press or escape + this.menu.addEventListener('keydown ', event => { + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + } }); }