From dcb5fcefc36a6a94e84e25e71b17984edc655228 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sat, 9 Mar 2019 18:29:36 +0100 Subject: [PATCH] Improved a11y of drop-downs See #2713 --- .../files/js/WoltLabSuite/Core/Event/Key.js | 10 ++ .../WoltLabSuite/Core/Ui/Dropdown/Simple.js | 107 +++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js index ba28b4bb03..336a328fa3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js @@ -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'. * 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 bf46326c8f..f8941e8d65 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js @@ -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(); + } } }; }); -- 2.20.1