diff --git a/resources/assets/javascripts/bootstrap/actionmenu.js b/resources/assets/javascripts/bootstrap/actionmenu.js
index 5cc6021404035999e7702965f95ad59be0fdaac7..bb6371dff39530d2edf6af7d7c6672116be2adb2 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 228d6c93b440b70b9fef57720c8578d301700bf1..a81f78abddbdf7cc4b9de13183221a12a6bd6ce4 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 918814cf5c4c2fec893f5447789189d4c86f2a12..9bce949d3b7ca3d0b233c315bd964142b7e09b13 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 e95b92dec0f95377f213774ae17d32df39cfaded..ebec6d9ccff3ca10514a9595b6379ce1fc24d1d4 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'],