From b27a5c7fb876c74c676eb9114383c25d82eb87fb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 24 Aug 2019 18:26:28 +0100 Subject: [PATCH] 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 --- app/Http/Controllers/PageController.php | 2 +- app/helpers.php | 5 +- .../js/components/breadcrumb-listing.js | 23 +---- .../assets/js/components/chapter-toggle.js | 2 + resources/assets/js/components/dropdown.js | 86 ++++++++++++++++--- resources/assets/js/components/overlay.js | 21 ++++- resources/assets/js/services/dom.js | 16 ++++ resources/assets/js/vues/code-editor.js | 4 +- resources/assets/sass/_buttons.scss | 9 +- resources/assets/sass/_components.scss | 6 +- resources/assets/sass/_forms.scss | 3 - resources/assets/sass/_header.scss | 17 ++-- resources/assets/sass/_html.scss | 5 ++ resources/assets/sass/_layout.scss | 4 + resources/assets/sass/_lists.scss | 11 ++- resources/lang/en/common.php | 5 ++ resources/lang/en/entities.php | 1 + resources/views/books/show.blade.php | 12 +-- resources/views/chapters/child-menu.blade.php | 9 +- resources/views/chapters/show.blade.php | 12 +-- resources/views/comments/comment.blade.php | 16 ++-- resources/views/common/header.blade.php | 9 +- .../views/components/code-editor.blade.php | 2 +- .../entity-selector-popup.blade.php | 2 +- .../views/components/expand-toggle.blade.php | 4 +- .../views/components/image-manager.blade.php | 2 +- resources/views/pages/form.blade.php | 4 +- resources/views/pages/revisions.blade.php | 9 +- resources/views/pages/show.blade.php | 12 +-- resources/views/partials/book-tree.blade.php | 10 ++- .../partials/breadcrumb-listing.blade.php | 7 +- .../entity-dashboard-search-box.blade.php | 7 +- .../partials/entity-export-menu.blade.php | 12 +++ .../partials/entity-list-item-basic.blade.php | 2 +- resources/views/partials/sort.blade.php | 7 +- 35 files changed, 227 insertions(+), 131 deletions(-) create mode 100644 resources/views/partials/entity-export-menu.blade.php diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 8819510a6..ad1e32665 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -495,7 +495,7 @@ class PageController extends Controller $revision->delete(); session()->flash('success', trans('entities.revision_delete_success')); - return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); + return redirect($page->getUrl('/revisions')); } /** diff --git a/app/helpers.php b/app/helpers.php index 9bbfcfbf0..f36f2e59d 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -133,8 +133,9 @@ function theme_path($path = '') : string function icon($name, $attrs = []) { $attrs = array_merge([ - 'class' => 'svg-icon', - 'data-icon' => $name + 'class' => 'svg-icon', + 'data-icon' => $name, + 'role' => 'presentation', ], $attrs); $attrString = ' '; foreach ($attrs as $attrName => $attr) { diff --git a/resources/assets/js/components/breadcrumb-listing.js b/resources/assets/js/components/breadcrumb-listing.js index 11e1522db..7f4344b17 100644 --- a/resources/assets/js/components/breadcrumb-listing.js +++ b/resources/assets/js/components/breadcrumb-listing.js @@ -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() { diff --git a/resources/assets/js/components/chapter-toggle.js b/resources/assets/js/components/chapter-toggle.js index a751206d1..bfd0ac729 100644 --- a/resources/assets/js/components/chapter-toggle.js +++ b/resources/assets/js/components/chapter-toggle.js @@ -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); } diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js index 3887e8432..e2bb21b0c 100644 --- a/resources/assets/js/components/dropdown.js +++ b/resources/assets/js/components/dropdown.js @@ -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(); + } }); } diff --git a/resources/assets/js/components/overlay.js b/resources/assets/js/components/overlay.js index 1ba5efcea..ad6a01061 100644 --- a/resources/assets/js/components/overlay.js +++ b/resources/assets/js/components/overlay.js @@ -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(); + } + } } diff --git a/resources/assets/js/services/dom.js b/resources/assets/js/services/dom.js index 797effd98..966a4540e 100644 --- a/resources/assets/js/services/dom.js +++ b/resources/assets/js/services/dom.js @@ -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. diff --git a/resources/assets/js/vues/code-editor.js b/resources/assets/js/vues/code-editor.js index d6f9965a8..c6df6b1a5 100644 --- a/resources/assets/js/vues/code-editor.js +++ b/resources/assets/js/vues/code-editor.js @@ -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); diff --git a/resources/assets/sass/_buttons.scss b/resources/assets/sass/_buttons.scss index eb7a09342..024b9cd7e 100644 --- a/resources/assets/sass/_buttons.scss +++ b/resources/assets/sass/_buttons.scss @@ -1,4 +1,6 @@ button { + background-color: transparent; + border: 0; font-size: 100%; } @@ -47,7 +49,12 @@ $button-border-radius: 2px; &:hover, &:focus { text-decoration: none; } + &:focus { + outline: 1px dotted currentColor; + outline-offset: -$-xs; + } &:active { + outline: 0; background-color: darken($primary, 8%); } } @@ -83,7 +90,7 @@ $button-border-radius: 2px; user-select: none; font-size: 0.75rem; line-height: 1.4em; - &:focus, &:active { + &:active { outline: 0; } &:hover { diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 0b683c6e3..a61c235eb 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -115,6 +115,9 @@ .popup-content { overflow-y: auto; } + &:focus { + outline: 0; + } } .popup-footer button, .popup-header-close { @@ -620,7 +623,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { opacity: 0; transition: opacity ease-in-out 120ms; } - &:hover .actions { + &:hover .actions, &:focus-within .actions { opacity: 1; } } @@ -637,7 +640,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } a { color: #666; } span { - color: #888; padding-left: $-xxs; } } diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 5fd19bb1f..3c81d6d9f 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -19,9 +19,6 @@ &.disabled, &[disabled] { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); } - &:focus { - outline: 0; - } } .fake-input { diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index adb014f4a..3cf55f1de 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -47,9 +47,7 @@ header { } .user-name { vertical-align: top; - padding-top: $-m; position: relative; - top: -3px; display: inline-block; cursor: pointer; > * { @@ -73,6 +71,9 @@ header { } } +.header *, .primary-background * { + outline-color: #FFF; +} .header-search { @@ -88,6 +89,10 @@ header .search-box { color: #EEE; z-index: 2; padding-left: 40px; + &:focus { + outline: none; + border: 1px solid rgba(255, 255, 255, 0.6); + } } button { fill: #EEE; @@ -103,12 +108,6 @@ header .search-box { ::-moz-placeholder { /* Firefox 19+ */ color: #DDD; } - :-ms-input-placeholder { /* IE 10+ */ - color: #DDD; - } - :-moz-placeholder { /* Firefox 18- */ - color: #DDD; - } @include between($l, $xl) { max-width: 200px; } @@ -243,7 +242,7 @@ header .search-box { line-height: 0.8; margin: -2px 0 0; } - &:hover { + &:hover, &:focus-within { opacity: 1; } } diff --git a/resources/assets/sass/_html.scss b/resources/assets/sass/_html.scss index 7c3a3c49b..de48c8ed1 100644 --- a/resources/assets/sass/_html.scss +++ b/resources/assets/sass/_html.scss @@ -1,5 +1,10 @@ * { box-sizing: border-box; + outline-color: #444444; +} + +*:focus { + outline-style: dotted; } html { diff --git a/resources/assets/sass/_layout.scss b/resources/assets/sass/_layout.scss index b282b12e2..381dc3ff3 100644 --- a/resources/assets/sass/_layout.scss +++ b/resources/assets/sass/_layout.scss @@ -307,7 +307,11 @@ body.flexbox { &:hover { opacity: 1; } + &:focus-within { + opacity: 1; + } } + } @include smaller-than($m) { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index b9b4d092a..74c36c86c 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -414,6 +414,11 @@ ul.pagination { background-color: transparent; border-color: rgba(0, 0, 0, 0.1); } + &:focus { + background-color: #eee; + outline: 1px dotted #666; + outline-offset: -2px; + } } .entity-list-item-path-sep { @@ -551,10 +556,14 @@ ul.pagination { color: #555; fill: #555; white-space: nowrap; - &:hover { + &:hover, &:focus { text-decoration: none; background-color: #EEE; } + &:focus { + outline: 1px solid rgba(0, 0, 0, 0.2); + outline-offset: -2px; + } svg { margin-right: $-s; display: inline-block; diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index ed880afcf..e81cf6a52 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -40,6 +40,10 @@ return [ 'add' => 'Add', // Sort Options + 'sort_options' => 'Sort Options', + 'sort_direction_toggle' => 'Sort Direction Toggle', + 'sort_ascending' => 'Sort Ascending', + 'sort_descending' => 'Sort Descending', 'sort_name' => 'Name', 'sort_created_at' => 'Created Date', 'sort_updated_at' => 'Updated Date', @@ -57,6 +61,7 @@ return [ 'default' => 'Default', // Header + 'profile_menu' => 'Profile Menu', 'view_profile' => 'View Profile', 'edit_profile' => 'Edit Profile', diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 3208a6dfc..b4fdbf0e5 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -176,6 +176,7 @@ return [ 'pages_delete_confirm' => 'Are you sure you want to delete this page?', 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', 'pages_editing_named' => 'Editing Page :pageName', + 'pages_edit_draft_options' => 'Draft Options', 'pages_edit_save_draft' => 'Save Draft', 'pages_edit_draft' => 'Edit Page Draft', 'pages_editing_draft' => 'Editing Draft', diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index b709b29dc..528eb5496 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -121,17 +121,7 @@
- + @include('partials.entity-export-menu', ['entity' => $book]) diff --git a/resources/views/chapters/child-menu.blade.php b/resources/views/chapters/child-menu.blade.php index 36c7f9a24..951825346 100644 --- a/resources/views/chapters/child-menu.blade.php +++ b/resources/views/chapters/child-menu.blade.php @@ -1,10 +1,11 @@
-

+ +

@stop diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index a85503178..9d7f230dc 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,8 +1,8 @@
-
-
- #{{$comment->local_id}} +
+
+ #{{$comment->local_id}}    @if ($comment->createdBy) {{ $comment->createdBy->name }} @@ -21,17 +21,17 @@
@if(userCan('comment-update', $comment)) - + @endif @if(userCan('comment-create-all')) - + @endif @if(userCan('comment-delete', $comment)) @endif diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index 95fcde659..f9e12014e 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -16,8 +16,8 @@