From 1d3345f105240a834ffca1678cfce0befae1f0b2 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Mon, 11 Mar 2019 17:58:19 +0100 Subject: [PATCH] Improved a11y of drop-down menus Keyboard navigation support for [End], [Escape] and [Home] See #2713 --- .../files/js/WoltLabSuite/Core/Event/Key.js | 38 ++++-- .../WoltLabSuite/Core/Ui/Dropdown/Simple.js | 109 +++++++++++------- 2 files changed, 97 insertions(+), 50 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js index 336a328fa3..6768d41383 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js @@ -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} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js index f8941e8d65..8b0fbd98d0 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js @@ -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'))); } }; }); -- 2.20.1