Fix accessibility issues in modal component
* Fix modal aria role * Trap focusing with tab / shift+tab inside the modal * Restore keyboard focus when closing modal * Automatically move keyboard focus to first focusable element unless specified otherwise * Keyboard shortcut help modal: move keyboard focus to modal title * Keyboard shortcut help modal: change close control from link to button
This commit is contained in:
parent
bfb4fc1c36
commit
29a06511a9
3 changed files with 77 additions and 6 deletions
|
@ -116,8 +116,8 @@
|
||||||
</main>
|
</main>
|
||||||
<template id="keyboard-shortcuts">
|
<template id="keyboard-shortcuts">
|
||||||
<div id="modal-left">
|
<div id="modal-left">
|
||||||
<a href="#" class="btn-close-modal">x</a>
|
<button class="btn-close-modal" aria-label="Close">x</button>
|
||||||
<h3>{{ t "page.keyboard_shortcuts.title" }}</h3>
|
<h3 tabindex="-1" id="dialog-title">{{ t "page.keyboard_shortcuts.title" }}</h3>
|
||||||
|
|
||||||
<div class="keyboard-shortcuts">
|
<div class="keyboard-shortcuts">
|
||||||
<p>{{ t "page.keyboard_shortcuts.subtitle.sections" }}</p>
|
<p>{{ t "page.keyboard_shortcuts.subtitle.sections" }}</p>
|
||||||
|
|
|
@ -94,7 +94,7 @@ function setFocusToSearchInput(event) {
|
||||||
function showKeyboardShortcuts() {
|
function showKeyboardShortcuts() {
|
||||||
let template = document.getElementById("keyboard-shortcuts");
|
let template = document.getElementById("keyboard-shortcuts");
|
||||||
if (template !== null) {
|
if (template !== null) {
|
||||||
ModalHandler.open(template.content);
|
ModalHandler.open(template.content, "dialog-title");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,29 +3,100 @@ class ModalHandler {
|
||||||
return document.getElementById("modal-container") !== null;
|
return document.getElementById("modal-container") !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static open(fragment) {
|
static getModalContainer() {
|
||||||
|
let container = document.getElementById("modal-container");
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFocusableElements() {
|
||||||
|
let container = this.getModalContainer();
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||||
|
}
|
||||||
|
|
||||||
|
static setupFocusTrap() {
|
||||||
|
let focusableElements = this.getFocusableElements();
|
||||||
|
|
||||||
|
if (focusableElements === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstFocusableElement = focusableElements[0];
|
||||||
|
let lastFocusableElement = focusableElements[focusableElements.length - 1];
|
||||||
|
|
||||||
|
this.getModalContainer().onkeydown = (e) => {
|
||||||
|
if (e.key !== 'Tab') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is only one focusable element in the dialog we always want to focus that one with the tab key.
|
||||||
|
// This handles the special case of having just one focusable element in a dialog where keyboard focus is placed on an element that is not in the tab order.
|
||||||
|
if (focusableElements.length === 1) {
|
||||||
|
firstFocusableElement.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey && document.activeElement === firstFocusableElement) {
|
||||||
|
lastFocusableElement.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === lastFocusableElement) {
|
||||||
|
firstFocusableElement.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(fragment, initialFocusElementId) {
|
||||||
if (ModalHandler.exists()) {
|
if (ModalHandler.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.activeElement = document.activeElement;
|
||||||
|
|
||||||
let container = document.createElement("div");
|
let container = document.createElement("div");
|
||||||
container.id = "modal-container";
|
container.id = "modal-container";
|
||||||
|
container.setAttribute("role", "dialog");
|
||||||
container.appendChild(document.importNode(fragment, true));
|
container.appendChild(document.importNode(fragment, true));
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
let closeButton = document.querySelector("a.btn-close-modal");
|
let closeButton = document.querySelector("button.btn-close-modal");
|
||||||
if (closeButton !== null) {
|
if (closeButton !== null) {
|
||||||
closeButton.onclick = (event) => {
|
closeButton.onclick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
ModalHandler.close();
|
ModalHandler.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initialFocusElement;
|
||||||
|
if (initialFocusElementId !== undefined) {
|
||||||
|
initialFocusElement = document.getElementById(initialFocusElementId);
|
||||||
|
} else {
|
||||||
|
initialFocusElement = this.getFocusableElements()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
initialFocusElement.focus();
|
||||||
|
|
||||||
|
this.setupFocusTrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
static close() {
|
static close() {
|
||||||
let container = document.getElementById("modal-container");
|
let container = this.getModalContainer();
|
||||||
if (container !== null) {
|
if (container !== null) {
|
||||||
container.parentNode.removeChild(container);
|
container.parentNode.removeChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.activeElement !== undefined) {
|
||||||
|
this.activeElement.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue