]> BookStack Code Mirror - bookstack/blobdiff - resources/assets/js/components/dropdown.js
Update settings.php
[bookstack] / resources / assets / js / components / dropdown.js
index 3887e8432289d23e2f81cd01424c7476428c8e33..4de1e239b93b3747bd7474ad188d7684b62dfb9e 100644 (file)
@@ -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();
+            }
         });
     }