Improved a11y of drop-down menus
authorAlexander Ebert <ebert@woltlab.com>
Mon, 11 Mar 2019 16:58:19 +0000 (17:58 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 11 Mar 2019 16:58:19 +0000 (17:58 +0100)
Keyboard navigation support for [End], [Escape] and [Home]

See #2713

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

index 336a328fa300b2f90a39cd39ac8f93494af6b8b4..6768d413835262d9d3da05bda5383733e43a5a4b 100644 (file)
@@ -23,7 +23,7 @@ define([], function() {
         */
        return {
                /**
-                * Returns true if pressed key equals 'ArrowDown'.
+                * Returns true if the pressed key equals 'ArrowDown'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -33,7 +33,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'ArrowLeft'.
+                * Returns true if the pressed key equals 'ArrowLeft'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -43,7 +43,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'ArrowRight'.
+                * Returns true if the pressed key equals 'ArrowRight'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -53,7 +53,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'ArrowUp'.
+                * Returns true if the pressed key equals 'ArrowUp'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -63,7 +63,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'Comma'.
+                * Returns true if the pressed key equals 'Comma'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -73,7 +73,17 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'Enter'.
+                * Returns true if the pressed key equals 'End'.
+                *
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               End: function(event) {
+                       return _isKey(event, 'End', 35);
+               },
+               
+               /**
+                * Returns true if the pressed key equals 'Enter'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -83,7 +93,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'Escape'.
+                * Returns true if the pressed key equals 'Escape'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -93,7 +103,17 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'Space'.
+                * Returns true if the pressed key equals 'Home'.
+                *
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               Home: function(event) {
+                       return _isKey(event, 'Home', 36);
+               },
+               
+               /**
+                * Returns true if the pressed key equals 'Space'.
                 *
                 * @param       {Event}         event           event object
                 * @return      {boolean}
@@ -103,7 +123,7 @@ define([], function() {
                },
                
                /**
-                * Returns true if pressed key equals 'Tab'.
+                * Returns true if the pressed key equals 'Tab'.
                 * 
                 * @param       {Event}         event           event object
                 * @return      {boolean}
index f8941e8d65ec532bb8f758e2cff19331ba6a177b..8b0fbd98d0c866c541cdec3bf8e6b192d9ebaf06 100644 (file)
@@ -210,7 +210,9 @@ define(
                                
                                // alignment
                                horizontal: (elData(dropdownMenu, 'dropdown-alignment-horizontal') === 'right') ? 'right' : 'left',
-                               vertical: (elData(dropdownMenu, 'dropdown-alignment-vertical') === 'top') ? 'top' : 'bottom'
+                               vertical: (elData(dropdownMenu, 'dropdown-alignment-vertical') === 'top') ? 'top' : 'bottom',
+                               
+                               allowFlip: elData(dropdownMenu, 'dropdown-allow-flip') || 'both'
                        });
                },
                
@@ -355,7 +357,10 @@ define(
                                                this.setAlignment(dropdown, _menus.get(containerId));
                                        }
                                        else {
-                                               this.close(containerId);
+                                               var menu = _menus.get(dropdown.id);
+                                               if (!elDataBool(menu, 'dropdown-ignore-page-scroll')) {
+                                                       this.close(containerId);
+                                               }
                                        }
                                }
                        }).bind(this));
@@ -491,11 +496,12 @@ define(
                                                elAttr(listItem, 'role', 'menuitem');
                                                elAttr(listItem, 'tabindex', -1);
                                        });
+                                       
+                                       this.setAlignment(dropdown, menu, alternateElement);
+                                       
                                        if (firstListItem !== null) {
                                                firstListItem.focus();
                                        }
-                                       
-                                       this.setAlignment(dropdown, menu, alternateElement);
                                }
                        }).bind(this));
                        
@@ -513,62 +519,83 @@ define(
                },
                
                _dropdownMenuKeyDown: function(event) {
-                       if (EventKey.ArrowDown(event) || EventKey.ArrowUp(event)) {
+                       var button, dropdown;
+                       
+                       var activeItem = document.activeElement;
+                       if (activeItem.nodeName !== 'LI') {
+                               return;
+                       }
+                       
+                       if (EventKey.ArrowDown(event) || EventKey.ArrowUp(event) || EventKey.End(event) || EventKey.Home(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 listItems = Array.prototype.slice.call(elBySelAll('li', activeItem.closest('.dropdownMenu')));
+                               if (EventKey.ArrowUp(event) || EventKey.End(event)) {
+                                       listItems.reverse();
+                               }
+                               var newActiveItem = null;
+                               var isValidItem = function(listItem) {
+                                       return !listItem.classList.contains('dropdownDivider') && listItem.clientHeight > 0;
+                               };
+                               
+                               var activeIndex = listItems.indexOf(activeItem);
+                               if (EventKey.End(event) || EventKey.Home(event)) {
+                                       activeIndex = -1;
+                               }
+                               
+                               for (var i = activeIndex + 1; i < listItems.length; i++) {
+                                       if (isValidItem(listItems[i])) {
+                                               newActiveItem = listItems[i];
+                                               break;
                                        }
-                                       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 (newActiveItem === null) {
+                                       for (i = 0; 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();
                                }
+                               
+                               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 target = activeItem;
+                               if (target.childElementCount === 1 && (target.children[0].nodeName === 'SPAN' || target.children[0].nodeName === 'A')) {
+                                       target = target.children[0];
+                               }
+                               
+                               dropdown = _dropdowns.get(_activeTargetId);
+                               button = elBySel('.dropdownToggle', dropdown);
+                               require(['Core'], function(Core) {
+                                       var mouseEvent = elData(dropdown, 'a11y-mouse-event') || 'click';
+                                       Core.triggerEvent(target, mouseEvent);
                                        
-                                       var dropdown = _dropdowns.get(_activeTargetId);
-                                       var button = elBySel('.dropdownToggle', dropdown);
-                                       target.click();
                                        if (button) button.focus();
-                               }
+                               });
                        }
-                       else if (EventKey.Tab(event)) {
+                       else if (EventKey.Escape(event) || EventKey.Tab(event)) {
                                event.preventDefault();
-                               var dropdown = _dropdowns.get(_activeTargetId);
-                               var button = elBySel('.dropdownToggle', dropdown);
+                               
+                               dropdown = _dropdowns.get(_activeTargetId);
+                               button = elBySel('.dropdownToggle', dropdown);
+                               // Remote controlled drop-down menus may not have a dedicated toggle button, instead the
+                               // `dropdown` element itself is the button.
+                               if (button === null && !dropdown.classList.contains('dropdown')) {
+                                       button = dropdown;
+                               }
+                               
                                this._toggle(null, _activeTargetId);
                                if (button) button.focus();
                        }
+               },
+               
+               _getNextVisibleItem: function(activeItem, index) {
+                       var items = Array.prototype.slice.call(elBySelAll('li', activeItem.closest('.dropdownMenu')));
                }
        };
 });