Improved a11y of drop-downs
authorMarcel Werk <burntime@woltlab.com>
Sat, 9 Mar 2019 17:29:36 +0000 (18:29 +0100)
committerMarcel Werk <burntime@woltlab.com>
Sat, 9 Mar 2019 17:29:36 +0000 (18:29 +0100)
See #2713

wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js

index ba28b4bb03346bad1f8d6c90e78bd746666fe6cb..336a328fa300b2f90a39cd39ac8f93494af6b8b4 100644 (file)
@@ -92,6 +92,16 @@ define([], function() {
                        return _isKey(event, 'Escape', 27);
                },
                
+               /**
+                * Returns true if pressed key equals 'Space'.
+                *
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               Space: function(event) {
+                       return _isKey(event, 'Space', 32);
+               },
+               
                /**
                 * Returns true if pressed key equals 'Tab'.
                 * 
index bf46326c8fc5a16b6e636690a93c3d5fdff99f77..f8941e8d65ec532bb8f758e2cff19331ba6a177b 100644 (file)
@@ -7,8 +7,8 @@
  * @module     WoltLabSuite/Core/Ui/Dropdown/Simple
  */
 define(
-       [       'CallbackList', 'Core', 'Dictionary', 'Ui/Alignment', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/CloseOverlay'],
-       function(CallbackList,   Core,   Dictionary,   UiAlignment,    DomChangeListener,    DomTraverse,    DomUtil,    UiCloseOverlay)
+       [       'CallbackList', 'Core', 'Dictionary', 'EventKey', 'Ui/Alignment', 'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'Ui/CloseOverlay'],
+       function(CallbackList,   Core,   Dictionary,   EventKey,   UiAlignment,    DomChangeListener,    DomTraverse,    DomUtil,    UiCloseOverlay)
 {
        "use strict";
        
@@ -18,6 +18,8 @@ define(
        var _dropdowns = new Dictionary();
        var _menus = new Dictionary();
        var _menuContainer = null;
+       var _callbackDropdownMenuKeyDown =  null;
+       var _activeTargetId = '';
        
        /**
         * @exports     WoltLabSuite/Core/Ui/Dropdown/Simple
@@ -45,6 +47,8 @@ define(
                        
                        // expose on window object for backward compatibility
                        window.bc_wcfSimpleDropdown = this;
+                       
+                       _callbackDropdownMenuKeyDown = this._dropdownMenuKeyDown.bind(this);
                },
                
                /**
@@ -65,6 +69,11 @@ define(
                init: function(button, isLazyInitialization) {
                        this.setup();
                        
+                       elAttr(button, 'role', 'button');
+                       elAttr(button, 'tabindex', '0');
+                       elAttr(button, 'aria-haspopup', true);
+                       elAttr(button, 'aria-expanded', false);
+                       
                        if (button.classList.contains('jsDropdownEnabled') || elData(button, 'target')) {
                                return false;
                        }
@@ -86,6 +95,7 @@ define(
                        if (!_dropdowns.has(containerId)) {
                                button.classList.add('jsDropdownEnabled');
                                button.addEventListener(WCF_CLICK_EVENT, this._toggle.bind(this));
+                               button.addEventListener('keydown', this._handleKeyDown.bind(this));
                                
                                _dropdowns.set(containerId, dropdown);
                                _menus.set(containerId, menu);
@@ -419,6 +429,7 @@ define(
                        }
                        
                        // close all dropdowns
+                       _activeTargetId = '';
                        _dropdowns.forEach((function(dropdown, containerId) {
                                var menu = _menus.get(containerId);
                                
@@ -427,13 +438,23 @@ define(
                                                dropdown.classList.remove('dropdownOpen');
                                                menu.classList.remove('dropdownOpen');
                                                
+                                               var button = elBySel('.dropdownToggle', dropdown);
+                                               if (button) elAttr(button, 'aria-expanded', false);
+                                               
                                                this._notifyCallbacks(containerId, 'close');
                                        }
+                                       else {
+                                               _activeTargetId = targetId;
+                                       }
                                }
                                else if (containerId === targetId && menu.childElementCount > 0) {
+                                       _activeTargetId = targetId;
                                        dropdown.classList.add('dropdownOpen');
                                        menu.classList.add('dropdownOpen');
                                        
+                                       var button = elBySel('.dropdownToggle', dropdown);
+                                       if (button) elAttr(button, 'aria-expanded', true);
+                                       
                                        if (menu.childElementCount && elDataBool(menu.children[0], 'scroll-to-active')) {
                                                var list = menu.children[0];
                                                list.removeAttribute('data-scroll-to-active');
@@ -458,6 +479,22 @@ define(
                                        
                                        this._notifyCallbacks(containerId, 'open');
                                        
+                                       elAttr(menu, 'role', 'menu');
+                                       elAttr(menu, 'tabindex', -1);
+                                       menu.removeEventListener('keydown', _callbackDropdownMenuKeyDown);
+                                       menu.addEventListener('keydown', _callbackDropdownMenuKeyDown);
+                                       var firstListItem = null;
+                                       elBySelAll('li', menu, function(listItem) {
+                                               if (firstListItem === null) firstListItem = listItem;
+                                               else if (listItem.classList.contains('active')) firstListItem = listItem;
+                                               
+                                               elAttr(listItem, 'role', 'menuitem');
+                                               elAttr(listItem, 'tabindex', -1);
+                                       });
+                                       if (firstListItem !== null) {
+                                               firstListItem.focus();
+                                       }
+                                       
                                        this.setAlignment(dropdown, menu, alternateElement);
                                }
                        }).bind(this));
@@ -466,6 +503,72 @@ define(
                        window.WCF.Dropdown.Interactive.Handler.closeAll();
                        
                        return (event === null);
+               },
+               
+               _handleKeyDown: function(event) {
+                       if (EventKey.Enter(event) || EventKey.Space(event)) {
+                               event.preventDefault();
+                               this._toggle(event);
+                       }
+               },
+               
+               _dropdownMenuKeyDown: function(event) {
+                       if (EventKey.ArrowDown(event) || EventKey.ArrowUp(event)) {
+                               event.preventDefault();
+                               
+                               var activeItem = document.activeElement;
+                               if (activeItem.nodeName === 'LI') {
+                                       var listItems = Array.prototype.slice.call(elBySelAll('li', activeItem.closest('.dropdownMenu')));
+                                       if (EventKey.ArrowUp(event)) {
+                                               listItems.reverse();
+                                       }
+                                       var activeIndex = listItems.indexOf(activeItem);
+                                       var newActiveItem = null;
+                                       
+                                       var isValidItem = function(listItem) {
+                                               return !listItem.classList.contains('dropdownDivider') && listItem.clientHeight > 0;
+                                       };
+                                       
+                                       for (var i = activeIndex + 1; i < listItems.length; i++) {
+                                               if (isValidItem(listItems[i])) {
+                                                       newActiveItem = listItems[i];
+                                                       break;
+                                               }
+                                       }
+                                       
+                                       if (newActiveItem === null) {
+                                               for (i = 0; i < listItems.length; i++) {
+                                                       if (isValidItem(listItems[i])) {
+                                                               newActiveItem = listItems[i];
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                                       newActiveItem.focus();
+                               }
+                       }
+                       else if (EventKey.Enter(event) || EventKey.Space(event)) {
+                               event.preventDefault();
+                               var activeItem = document.activeElement;
+                               if (activeItem.nodeName === 'LI') {
+                                       var target = activeItem;
+                                       if (target.childElementCount === 1 && (target.children[0].nodeName === 'SPAN' || target.children[0].nodeName === 'A')) {
+                                               target = target.children[0];
+                                       }
+                                       
+                                       var dropdown = _dropdowns.get(_activeTargetId);
+                                       var button = elBySel('.dropdownToggle', dropdown);
+                                       target.click();
+                                       if (button) button.focus();
+                               }
+                       }
+                       else if (EventKey.Tab(event)) {
+                               event.preventDefault();
+                               var dropdown = _dropdowns.get(_activeTargetId);
+                               var button = elBySel('.dropdownToggle', dropdown);
+                               this._toggle(null, _activeTargetId);
+                               if (button) button.focus();
+                       }
                }
        };
 });