Fix userbar tabbing behaviour

Tabbing (navigation using Tab or Shift + Tab keys) will now close
the menu and move to the next focusable element on the page instead
of focusing the next menu item.

The previous behaviour was a deviation from the ARIA menu practices:
https://w3c.github.io/aria-practices/#menu

Further changes / cleanup:

* Consume keyboard events like arrow down to prevent the browser
  from interpreting them.
* Refactor repeated setTimeout and `.focus()` calls into single
  `focusElement(el)` function. Let's keep it DRY!

Fixes #7290
pull/7527/head
Storm Heg 2021-09-08 23:53:42 +02:00 zatwierdzone przez LB (Ben Johnston)
rodzic a96035a619
commit 19ad01ddd5
3 zmienionych plików z 67 dodań i 37 usunięć

Wyświetl plik

@ -43,6 +43,7 @@ Changelog
* Fix: Disable Task confirmation now shows the correct value for quantity of tasks in progress (LB Johnston)
* Fix: Page history now works correctly when it contains changes by a deleted user (Dan Braghis)
* Fix: Add `gettext_lazy` to `ModelAdmin` built in view titles so that language settings are correctly used (Matt Westcott)
* Fix: Tabbing and keyboard interaction on the Wagtail userbar now aligns with ARIA best practices (Storm Heg)
2.14.1 (12.08.2021)

Wyświetl plik

@ -2,6 +2,9 @@
// Please stick to old JS APIs and avoid importing anything that might require a vendored module
// More background can be found in webpack.config.js
// This component implements a roving tab index for keyboard navigation
// Learn more about roving tabIndex: https://w3c.github.io/aria-practices/#kbd_roving_tabindex
document.addEventListener('DOMContentLoaded', () => {
const userbar = document.querySelector('[data-wagtail-userbar]');
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
@ -9,12 +12,12 @@ document.addEventListener('DOMContentLoaded', () => {
const listItems = list.querySelectorAll('li');
const isActiveClass = 'is-active';
// querySelector for all items that can be focused.
// querySelector for all items that can be focused
// tabIndex has been removed for roving tabindex compatibility
// source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
const focusableItemSelector = `a[href]:not([tabindex='-1']),
button:not([disabled]):not([tabindex='-1']),
input:not([disabled]):not([tabindex='-1']),
[tabindex]:not([tabindex='-1'])`;
const focusableItemSelector = `a[href],
button:not([disabled]),
input:not([disabled])`;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
trigger.addEventListener('click', toggleUserbar, false);
@ -26,8 +29,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Handle keyboard events on the trigger
// eslint-disable-next-line @typescript-eslint/no-use-before-define
userbar.addEventListener('keydown', handleTriggerKeyDown);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
list.addEventListener('focusout', handleFocusChange);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
resetItemsTabIndex(); // On initialisation, all menu items should be disabled for roving tab index
function showUserbar(shouldFocus) {
userbar.classList.add(isActiveClass);
trigger.setAttribute('aria-expanded', 'true');
@ -76,27 +84,42 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function setFocusToTrigger() {
setTimeout(() => trigger.focus(), 300);
}
function isFocusOnItems() {
return document.activeElement && !!document.activeElement.closest('.wagtail-userbar-items');
}
/** Reset all focusable menu items to `tabIndex = -1` */
function resetItemsTabIndex() {
listItems.forEach(listItem => {
// eslint-disable-next-line no-param-reassign
listItem.firstElementChild.tabIndex = -1;
});
}
/** Focus element using a roving tab index */
function focusElement(el) {
resetItemsTabIndex();
// eslint-disable-next-line no-param-reassign
el.tabIndex = 0;
setTimeout(() => {
el.focus();
}, 100); // Workaround, changing focus only works after a timeout
}
function setFocusToTrigger() {
setTimeout(() => trigger.focus(), 300);
resetItemsTabIndex();
}
function setFocusToFirstItem() {
if (listItems.length > 0) {
setTimeout(() => {
listItems[0].firstElementChild.focus();
}, 100); // Workaround for focus bug
focusElement(listItems[0].firstElementChild);
}
}
function setFocusToLastItem() {
if (listItems.length > 0) {
setTimeout(() => {
listItems[listItems.length - 1].firstElementChild.focus();
}, 100); // Workaround for focus bug
focusElement(listItems[listItems.length - 1].firstElementChild);
}
}
@ -104,30 +127,24 @@ document.addEventListener('DOMContentLoaded', () => {
listItems.forEach((element, idx) => {
// Check which item is currently focused
if (element.firstElementChild === document.activeElement) {
setTimeout(() => {
if (idx + 1 < listItems.length) {
// Focus the next item
listItems[idx + 1].firstElementChild.focus();
} else {
setFocusToFirstItem();
}
}, 100); // Workaround for focus bug
if (idx + 1 < listItems.length) {
focusElement(listItems[idx + 1].firstElementChild);
} else { // Loop around
setFocusToFirstItem();
}
}
});
}
function setFocusToPreviousItem() {
// Check which item is currently focused
listItems.forEach((element, idx) => {
// Check which item is currently focused
if (element.firstElementChild === document.activeElement) {
setTimeout(() => {
if (idx > 0) {
// Focus the previous item
listItems[idx - 1].firstElementChild.focus();
} else {
setFocusToLastItem();
}
}, 100); // Workaround for focus bug
if (idx > 0) {
focusElement(listItems[idx - 1].firstElementChild);
} else {
setFocusToLastItem();
}
}
});
}
@ -146,33 +163,44 @@ document.addEventListener('DOMContentLoaded', () => {
if (event.key === 'Escape') {
hideUserbar();
setFocusToTrigger();
return;
return false;
}
// List items are in focus, move focus if needed
if (isFocusOnItems()) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setFocusToNextItem();
break;
return false;
case 'ArrowUp':
event.preventDefault();
setFocusToPreviousItem();
break;
return false;
case 'Home':
event.preventDefault();
setFocusToFirstItem();
break;
return false;
case 'End':
event.preventDefault();
setFocusToLastItem();
break;
return false;
default:
break;
}
}
}
return true;
}
function handleFocusChange(event) {
// Is the focus is still in the menu? If so, don't to anything
if (event.relatedTarget == null || (event.relatedTarget && event.relatedTarget.closest('.wagtail-userbar-items'))) {
return;
}
// List items not in focus - the menu should close
resetItemsTabIndex();
hideUserbar();
}
/**

Wyświetl plik

@ -58,6 +58,7 @@ Bug fixes
* Disable Task confirmation now shows the correct value for quantity of tasks in progress (LB Johnston)
* Page history now works correctly when it contains changes by a deleted user (Dan Braghis)
* Add ``gettext_lazy`` to ``ModelAdmin`` built in view titles so that language settings are correctly used (Matt Westcott)
* Tabbing and keyboard interaction on the Wagtail userbar now aligns with ARIA best practices (Storm Heg)
Upgrade considerations
======================