kopia lustrzana https://github.com/wagtail/wagtail
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 #7290pull/7527/head
rodzic
a96035a619
commit
19ad01ddd5
|
@ -43,6 +43,7 @@ Changelog
|
||||||
* Fix: Disable Task confirmation now shows the correct value for quantity of tasks in progress (LB Johnston)
|
* 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: 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: 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)
|
2.14.1 (12.08.2021)
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
// Please stick to old JS APIs and avoid importing anything that might require a vendored module
|
// 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
|
// 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const userbar = document.querySelector('[data-wagtail-userbar]');
|
const userbar = document.querySelector('[data-wagtail-userbar]');
|
||||||
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
|
const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
|
||||||
|
@ -9,12 +12,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const listItems = list.querySelectorAll('li');
|
const listItems = list.querySelectorAll('li');
|
||||||
const isActiveClass = 'is-active';
|
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
|
// source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
|
||||||
const focusableItemSelector = `a[href]:not([tabindex='-1']),
|
const focusableItemSelector = `a[href],
|
||||||
button:not([disabled]):not([tabindex='-1']),
|
button:not([disabled]),
|
||||||
input:not([disabled]):not([tabindex='-1']),
|
input:not([disabled])`;
|
||||||
[tabindex]:not([tabindex='-1'])`;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
trigger.addEventListener('click', toggleUserbar, false);
|
trigger.addEventListener('click', toggleUserbar, false);
|
||||||
|
@ -26,8 +29,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Handle keyboard events on the trigger
|
// Handle keyboard events on the trigger
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
userbar.addEventListener('keydown', handleTriggerKeyDown);
|
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) {
|
function showUserbar(shouldFocus) {
|
||||||
userbar.classList.add(isActiveClass);
|
userbar.classList.add(isActiveClass);
|
||||||
trigger.setAttribute('aria-expanded', 'true');
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
@ -76,27 +84,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocusToTrigger() {
|
|
||||||
setTimeout(() => trigger.focus(), 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFocusOnItems() {
|
function isFocusOnItems() {
|
||||||
return document.activeElement && !!document.activeElement.closest('.wagtail-userbar-items');
|
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() {
|
function setFocusToFirstItem() {
|
||||||
if (listItems.length > 0) {
|
if (listItems.length > 0) {
|
||||||
setTimeout(() => {
|
focusElement(listItems[0].firstElementChild);
|
||||||
listItems[0].firstElementChild.focus();
|
|
||||||
}, 100); // Workaround for focus bug
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocusToLastItem() {
|
function setFocusToLastItem() {
|
||||||
if (listItems.length > 0) {
|
if (listItems.length > 0) {
|
||||||
setTimeout(() => {
|
focusElement(listItems[listItems.length - 1].firstElementChild);
|
||||||
listItems[listItems.length - 1].firstElementChild.focus();
|
|
||||||
}, 100); // Workaround for focus bug
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,30 +127,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
listItems.forEach((element, idx) => {
|
listItems.forEach((element, idx) => {
|
||||||
// Check which item is currently focused
|
// Check which item is currently focused
|
||||||
if (element.firstElementChild === document.activeElement) {
|
if (element.firstElementChild === document.activeElement) {
|
||||||
setTimeout(() => {
|
|
||||||
if (idx + 1 < listItems.length) {
|
if (idx + 1 < listItems.length) {
|
||||||
// Focus the next item
|
focusElement(listItems[idx + 1].firstElementChild);
|
||||||
listItems[idx + 1].firstElementChild.focus();
|
} else { // Loop around
|
||||||
} else {
|
|
||||||
setFocusToFirstItem();
|
setFocusToFirstItem();
|
||||||
}
|
}
|
||||||
}, 100); // Workaround for focus bug
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocusToPreviousItem() {
|
function setFocusToPreviousItem() {
|
||||||
// Check which item is currently focused
|
|
||||||
listItems.forEach((element, idx) => {
|
listItems.forEach((element, idx) => {
|
||||||
|
// Check which item is currently focused
|
||||||
if (element.firstElementChild === document.activeElement) {
|
if (element.firstElementChild === document.activeElement) {
|
||||||
setTimeout(() => {
|
|
||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
// Focus the previous item
|
focusElement(listItems[idx - 1].firstElementChild);
|
||||||
listItems[idx - 1].firstElementChild.focus();
|
|
||||||
} else {
|
} else {
|
||||||
setFocusToLastItem();
|
setFocusToLastItem();
|
||||||
}
|
}
|
||||||
}, 100); // Workaround for focus bug
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -146,33 +163,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
hideUserbar();
|
hideUserbar();
|
||||||
setFocusToTrigger();
|
setFocusToTrigger();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List items are in focus, move focus if needed
|
||||||
if (isFocusOnItems()) {
|
if (isFocusOnItems()) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocusToNextItem();
|
setFocusToNextItem();
|
||||||
break;
|
return false;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocusToPreviousItem();
|
setFocusToPreviousItem();
|
||||||
break;
|
return false;
|
||||||
case 'Home':
|
case 'Home':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocusToFirstItem();
|
setFocusToFirstItem();
|
||||||
break;
|
return false;
|
||||||
case 'End':
|
case 'End':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocusToLastItem();
|
setFocusToLastItem();
|
||||||
break;
|
return false;
|
||||||
default:
|
default:
|
||||||
break;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
// List items not in focus - the menu should close
|
||||||
|
resetItemsTabIndex();
|
||||||
|
hideUserbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -58,6 +58,7 @@ Bug fixes
|
||||||
* Disable Task confirmation now shows the correct value for quantity of tasks in progress (LB Johnston)
|
* 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)
|
* 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)
|
* 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
|
Upgrade considerations
|
||||||
======================
|
======================
|
||||||
|
|
Ładowanie…
Reference in New Issue