From 8bc88f75ea82dcdcf218e5114d12f72d6b37cf3b Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Tue, 1 Oct 2024 11:04:49 +0000 Subject: [PATCH] rework keyboard focus management for action menues, fixes #4641 Closes #4641 Merge request studip/studip!3457 --- .../javascripts/bootstrap/actionmenu.js | 15 ---- .../assets/javascripts/lib/actionmenu.js | 73 +++++++++++-------- resources/vue/components/StudipActionMenu.vue | 2 +- templates/shared/action-menu.php | 2 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/resources/assets/javascripts/bootstrap/actionmenu.js b/resources/assets/javascripts/bootstrap/actionmenu.js index 5cc60214040..bb6371dff39 100644 --- a/resources/assets/javascripts/bootstrap/actionmenu.js +++ b/resources/assets/javascripts/bootstrap/actionmenu.js @@ -34,19 +34,4 @@ STUDIP.ActionMenu.closeAll(); } }); - - // Close all action menus when the escape key is pressed and rotate through all its items - // when TAB or SHIFT + TAB is pressed. - $(document).on('keydown', function(event) { - if (event.key === 'Escape') { - STUDIP.ActionMenu.closeAll(); - } else if (event.key === 'Tab') { - //Check if the focus is inside an action menu: - let menu = $(event.target).closest('.action-menu'); - if (menu.hasClass('is-open') && STUDIP.ActionMenu.tabThroughItems(menu, event.shiftKey)) { - event.preventDefault(); - } - } - }); - }(jQuery)); diff --git a/resources/assets/javascripts/lib/actionmenu.js b/resources/assets/javascripts/lib/actionmenu.js index 228d6c93b44..a81f78abddb 100644 --- a/resources/assets/javascripts/lib/actionmenu.js +++ b/resources/assets/javascripts/lib/actionmenu.js @@ -100,7 +100,7 @@ class ActionMenu const additionalClasses = Object.values({ ...this.element[0].classList }).filter((item) => item != 'action-menu'); const menu_width = this.content.width(); const menu_height = this.content.height(); - + // Reposition the menu? if (position) { let parents = getScrollableParents(this.element, menu_width, menu_height); @@ -122,9 +122,28 @@ class ActionMenu this.position = false; } } + + this.attachEventHandlers(); + this.update(); } + // Close all action menus when the escape key is pressed and rotate through all its items + // when TAB or SHIFT + TAB is pressed. + attachEventHandlers() { + this.menu[0].addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + this.close(true); + } else if (event.key === 'Tab' && this.is_open) { + this.tabThroughItems(event.shiftKey); + event.preventDefault(); + } else if (event.key === 'Enter' && event.target.matches('label')) { + event.target.querySelector('button,input').click(); + event.preventDefault(); + } + }); + } + toggleScrollHandler(active) { if (ActionMenu.scrollHandlerState === active) { return; @@ -170,19 +189,23 @@ class ActionMenu /** * Toggle the menus display state. Pass a state to enforce it. */ - toggle(state = null) { + toggle(state = null, focus = false) { this.is_open = state === null ? !this.is_open : state; this.update(); if (this.is_open) { this.reposition(); - this.menu.find('.action-menu-icon').focus(); ActionMenu.openMenus.push(this); } else { ActionMenu.openMenus = ActionMenu.openMenus.filter(menu => menu !== this); } + // Always focus the toggle element + if (this.is_open || focus) { + this.menu.find('.action-menu-icon').focus(); + } + this.toggleScrollHandler(ActionMenu.openMenus.filter(menu => menu.position).length > 0); } @@ -237,36 +260,28 @@ class ActionMenu * Handles the rotation through the action menu items when the first * or last element of the menu has been reached. * - * @param menu The menu whose items shall be rotated through. - * * @param reverse Whether to rotate in reverse (true) or not (false). * Defaults to false. */ - static tabThroughItems(menu, reverse = false) { - if (reverse) { - //Put the focus on the last link in the menu, if the first link has the focus: - if (jQuery(menu).find('a:first:focus').length > 0) { - //Put the focus on the action menu button: - jQuery(menu).find('button.action-menu-icon').focus(); - return true; - } else if (jQuery(menu).find('button.action-menu-icon:focus').length > 0) { - //Put the focus on the last action menu item: - jQuery(menu).find('a:last').focus(); - return true; - } - } else { - //Put the focus on the first link in the menu, if the last link has the focus: - if (jQuery(menu).find('a:last:focus').length > 0) { - //Put the focus on the action menu button: - jQuery(menu).find('button.action-menu-icon').focus(); - return true; - } else if (jQuery(menu).find('button.action-menu-icon:focus').length > 0) { - //Put the focus on the first action menu item: - jQuery(menu).find('a:first').focus(); - return true; - } + tabThroughItems(reverse = false) { + const items = Array.from(this.menu[0].querySelectorAll([ + '.action-menu-icon', + '.action-menu-item:not(.action-menu-item-disabled) a', + '.action-menu-item:not(.action-menu-item-disabled) button', + '.action-menu-item:not(.action-menu-item-disabled) label', + ].join(','))); + + // Get index of currently focussed element + let index = items.findIndex(element => element === document.activeElement); + if (index === -1) { + index = 0; } - return false; + + // Get new index based on direction + index = (index + (reverse ? -1 : 1) + items.length) % items.length; + + // Focus element + items[index].focus(); } } diff --git a/resources/vue/components/StudipActionMenu.vue b/resources/vue/components/StudipActionMenu.vue index 918814cf5c4..9bce949d3b7 100644 --- a/resources/vue/components/StudipActionMenu.vue +++ b/resources/vue/components/StudipActionMenu.vue @@ -33,7 +33,7 @@ <span v-else class="action-menu-no-icon"></span> {{ item.label }} </a> - <label v-else-if="item.icon" class="undecorated" v-on="linkEvents(item)"> + <label v-else-if="item.icon" class="undecorated" v-on="linkEvents(item)" tabindex="0"> <studip-icon :shape="item.icon" :name="item.name" class="action-menu-item-icon" diff --git a/templates/shared/action-menu.php b/templates/shared/action-menu.php index e95b92dec0f..ebec6d9ccff 100644 --- a/templates/shared/action-menu.php +++ b/templates/shared/action-menu.php @@ -49,7 +49,7 @@ </a> <? elseif ($action['type'] === 'button'): ?> <? if ($action['icon']): ?> - <label class="undecorated"> + <label class="undecorated" tabindex="0"> <?= $action['icon']->asInput(false, $action['attributes'] + [ 'class' => 'action-menu-item-icon', 'name' => $action['name'], -- GitLab