1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-08-09 10:22:51 +03:00

Made a mass of accessibility improvements

- Changed default focus styles
- Updated dropdowns with keyboard navigation
- Updated modals with esc exiting
- Added accessibility attirbutes where needed
- Made many more elements focusable
- Updated hover effects of many items to also apply when focused within

Related to #1320 and #1198
This commit is contained in:
Dan Brown
2019-08-24 18:26:28 +01:00
parent 1b33a0c5b9
commit b27a5c7fb8
35 changed files with 227 additions and 131 deletions

View File

@@ -7,35 +7,14 @@ class BreadcrumbListing {
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
this.toggleElem.addEventListener('click', this.onShow.bind(this));
this.elem.addEventListener('show', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
this.elem.addEventListener('keydown', this.keyDown.bind(this));
}
keyDown(event) {
if (event.key === 'ArrowDown') {
this.listFocusChange(1);
event.preventDefault();
} else if (event.key === 'ArrowUp') {
this.listFocusChange(-1);
event.preventDefault();
}
}
listFocusChange(indexChange = 1) {
const links = Array.from(this.entityListElem.querySelectorAll('a:not(.hidden)'));
const currentFocused = this.entityListElem.querySelector('a:focus');
const currentFocusedIndex = links.indexOf(currentFocused);
const defaultFocus = (indexChange > 0) ? links[0] : this.searchInput;
const nextElem = links[currentFocusedIndex + indexChange] || defaultFocus;
nextElem.focus();
}
onShow() {

View File

@@ -11,12 +11,14 @@ class ChapterToggle {
open() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.add('open');
this.elem.setAttribute('aria-expanded', 'true');
slideDown(list, 240);
}
close() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.remove('open');
this.elem.setAttribute('aria-expanded', 'false');
slideUp(list, 240);
}

View File

@@ -1,3 +1,5 @@
import {onSelect} from "../services/dom";
/**
* Dropdown
* Provides some simple logic to create simple dropdown menus.
@@ -10,14 +12,16 @@ class DropDown {
this.moveMenu = elem.hasAttribute('dropdown-move-menu');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.body = document.body;
this.showing = false;
this.setupListeners();
}
show(event) {
show(event = null) {
this.hideAll();
this.menu.style.display = 'block';
this.menu.classList.add('anim', 'menuIn');
this.toggle.setAttribute('aria-expanded', 'true');
if (this.moveMenu) {
// Move to body to prevent being trapped within scrollable sections
@@ -38,10 +42,17 @@ class DropDown {
});
// Focus on first input if existing
let input = this.menu.querySelector('input');
const input = this.menu.querySelector('input');
if (input !== null) input.focus();
event.stopPropagation();
this.showing = true;
const showEvent = new Event('show');
this.container.dispatchEvent(showEvent);
if (event) {
event.stopPropagation();
}
}
hideAll() {
@@ -53,6 +64,7 @@ class DropDown {
hide() {
this.menu.style.display = 'none';
this.menu.classList.remove('anim', 'menuIn');
this.toggle.setAttribute('aria-expanded', 'false');
if (this.moveMenu) {
this.menu.style.position = '';
this.menu.style.left = '';
@@ -60,22 +72,74 @@ class DropDown {
this.menu.style.width = '';
this.container.appendChild(this.menu);
}
this.showing = false;
}
getFocusable() {
return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])'));
}
focusNext() {
const focusable = this.getFocusable();
const currentIndex = focusable.indexOf(document.activeElement);
let newIndex = currentIndex + 1;
if (newIndex >= focusable.length) {
newIndex = 0;
}
focusable[newIndex].focus();
}
focusPrevious() {
const focusable = this.getFocusable();
const currentIndex = focusable.indexOf(document.activeElement);
let newIndex = currentIndex - 1;
if (newIndex < 0) {
newIndex = focusable.length - 1;
}
focusable[newIndex].focus();
}
setupListeners() {
// Hide menu on option click
this.container.addEventListener('click', event => {
let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.indexOf(event.target) !== -1) this.hide();
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.includes(event.target)) {
this.hide();
}
});
// Show dropdown on toggle click
this.toggle.addEventListener('click', this.show.bind(this));
// Hide menu on enter press
this.container.addEventListener('keypress', event => {
if (event.keyCode !== 13) return true;
onSelect(this.toggle, event => {
event.stopPropagation();
console.log('cat', event);
this.show(event);
if (event instanceof KeyboardEvent) {
this.focusNext();
}
});
// Arrow navigation
this.container.addEventListener('keydown', event => {
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
this.focusNext();
event.preventDefault();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
this.focusPrevious();
event.preventDefault();
} else if (event.key === 'Escape') {
this.hide();
return false;
event.stopPropagation();
}
});
// Hide menu on enter press or escape
this.menu.addEventListener('keydown ', event => {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
this.hide();
}
});
}

View File

@@ -6,12 +6,22 @@ class Overlay {
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
window.addEventListener('keyup', event => {
if (event.key === 'Escape') {
this.hide();
}
});
let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
hide() { this.toggle(false); }
show() { this.toggle(true); }
toggle(show = true) {
let start = Date.now();
let duration = 240;
@@ -22,6 +32,9 @@ class Overlay {
this.container.style.opacity = targetOpacity;
if (elapsedTime > duration) {
this.container.style.display = show ? 'flex' : 'none';
if (show) {
this.focusOnBody();
}
this.container.style.opacity = '';
} else {
requestAnimationFrame(setOpacity.bind(this));
@@ -31,8 +44,12 @@ class Overlay {
requestAnimationFrame(setOpacity.bind(this));
}
hide() { this.toggle(false); }
show() { this.toggle(true); }
focusOnBody() {
const body = this.container.querySelector('.popup-body');
if (body) {
body.focus();
}
}
}

View File

@@ -22,6 +22,22 @@ export function onEvents(listenerElement, events, callback) {
}
}
/**
* Helper to run an action when an element is selected.
* A "select" is made to be accessible, So can be a click, space-press or enter-press.
* @param listenerElement
* @param callback
*/
export function onSelect(listenerElement, callback) {
listenerElement.addEventListener('click', callback);
listenerElement.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
callback(event);
}
});
}
/**
* Set a listener on an element for an event emitted by a child
* matching the given childSelector param.

View File

@@ -3,10 +3,10 @@ import codeLib from "../services/code";
const methods = {
show() {
if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
this.$refs.overlay.style.display = 'flex';
this.$refs.overlay.components.overlay.show();
},
hide() {
this.$refs.overlay.style.display = 'none';
this.$refs.overlay.components.overlay.hide();
},
updateEditorMode(language) {
codeLib.setMode(this.editor, language);