]> BookStack Code Mirror - bookstack/commitdiff
Improved header element accessibility when at mobile sizes
authorDan Brown <redacted>
Mon, 19 Apr 2021 20:41:13 +0000 (21:41 +0100)
committerDan Brown <redacted>
Mon, 19 Apr 2021 20:41:13 +0000 (21:41 +0100)
Intended to fix issues raised in #2681.
Changes up the tri-layout tabs, and the main header menu toggle,
to be buttons while adding better text and keyboard controls.

Updated the component format of a few elements along the way.

resources/js/components/dropdown.js
resources/js/components/header-mobile-toggle.js
resources/js/components/tri-layout.js
resources/lang/en/common.php
resources/sass/_header.scss
resources/views/base.blade.php
resources/views/common/header.blade.php
resources/views/tri-layout.blade.php

index 22402d483902b2148918150e1d371611dd1abf9b..f761ecf011541590963caf66daf4b145f43c8cde 100644 (file)
@@ -12,6 +12,7 @@ class DropDown {
         this.menu = this.$refs.menu;
         this.toggle = this.$refs.toggle;
         this.moveMenu = this.$opts.moveMenu;
+        this.bubbleEscapes = this.$opts.bubbleEscapes === 'true';
 
         this.direction = (document.dir === 'rtl') ? 'right' : 'left';
         this.body = document.body;
@@ -137,7 +138,9 @@ class DropDown {
             } else if (event.key === 'Escape') {
                 this.hide();
                 this.toggle.focus();
-                event.stopPropagation();
+                if (!this.bubbleEscapes) {
+                    event.stopPropagation();
+                }
             }
         };
         this.container.addEventListener('keydown', keyboardNavigation);
index eccd4b8f0a6b25e722b1754cd6c611db1ed3ac64..99737bfb8b0fb741564a5c854d566fd3f774067a 100644 (file)
@@ -1,31 +1,41 @@
 
 class HeaderMobileToggle {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.toggleButton = elem.querySelector('.mobile-menu-toggle');
-        this.menu = elem.querySelector('.header-links');
-        this.open = false;
+    setup() {
+        this.elem = this.$el;
+        this.toggleButton = this.$refs.toggle;
+        this.menu = this.$refs.menu;
 
+        this.open = false;
         this.toggleButton.addEventListener('click', this.onToggle.bind(this));
         this.onWindowClick = this.onWindowClick.bind(this);
+        this.onKeyDown = this.onKeyDown.bind(this);
     }
 
     onToggle(event) {
         this.open = !this.open;
         this.menu.classList.toggle('show', this.open);
+        this.toggleButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');
         if (this.open) {
+            this.elem.addEventListener('keydown', this.onKeyDown);
             window.addEventListener('click', this.onWindowClick)
         } else {
+            this.elem.removeEventListener('keydown', this.onKeyDown);
             window.removeEventListener('click', this.onWindowClick)
         }
         event.stopPropagation();
     }
 
+    onKeyDown(event) {
+        if (event.code === 'Escape') {
+            this.onToggle(event);
+        }
+    }
+
     onWindowClick(event) {
         this.onToggle(event);
     }
 
 }
 
-module.exports = HeaderMobileToggle;
\ No newline at end of file
+export default HeaderMobileToggle;
\ No newline at end of file
index 905ca03b1020d566859366d6e2ecc8e851edc784..f801e52a193715fdea427fa4eac03c2372ca9abf 100644 (file)
@@ -1,8 +1,9 @@
 
 class TriLayout {
 
-    constructor(elem) {
-        this.elem = elem;
+    setup() {
+        this.container = this.$refs.container;
+        this.tabs = this.$manyRefs.tab;
 
         this.lastLayoutType = 'none';
         this.onDestroy = null;
@@ -43,13 +44,12 @@ class TriLayout {
     }
 
     setupMobile() {
-        const layoutTabs = document.querySelectorAll('[tri-layout-mobile-tab]');
-        for (let tab of layoutTabs) {
+        for (const tab of this.tabs) {
             tab.addEventListener('click', this.mobileTabClick);
         }
 
         this.onDestroy = () => {
-            for (let tab of layoutTabs) {
+            for (const tab of this.tabs) {
                 tab.removeEventListener('click', this.mobileTabClick);
             }
         }
@@ -65,7 +65,7 @@ class TriLayout {
      * @param event
      */
     mobileTabClick(event) {
-        const tab = event.target.getAttribute('tri-layout-mobile-tab');
+        const tab = event.target.dataset.tab;
         this.showTab(tab);
     }
 
@@ -79,21 +79,21 @@ class TriLayout {
 
     /**
      * Show the given tab
-     * @param tabName
+     * @param {String} tabName
+     * @param {Boolean }scroll
      */
     showTab(tabName, scroll = true) {
         this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
 
         // Set tab status
-        const tabs = document.querySelectorAll('.tri-layout-mobile-tab');
-        for (let tab of tabs) {
-            const isActive = (tab.getAttribute('tri-layout-mobile-tab') === tabName);
-            tab.classList.toggle('active', isActive);
+        for (const tab of this.tabs) {
+            const isActive = (tab.dataset.tab === tabName);
+            tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
         }
 
         // Toggle section
         const showInfo = (tabName === 'info');
-        this.elem.classList.toggle('show-info', showInfo);
+        this.container.classList.toggle('show-info', showInfo);
 
         // Set the scroll position from cache
         if (scroll) {
index 3b003c64ad814ad97d36912c366850995c97488e..855c1c807488a8af9860f35f24004e0bb055edc1 100644 (file)
@@ -65,6 +65,7 @@ return [
     'breadcrumb' => 'Breadcrumb',
 
     // Header
+    'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Profile Menu',
     'view_profile' => 'View Profile',
     'edit_profile' => 'Edit Profile',
@@ -73,7 +74,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Info',
+    'tab_info_label' => 'Tab: Show Secondary Information',
     'tab_content' => 'Content',
+    'tab_content_label' => 'Tab: Show Primary Content',
 
     // Email Content
     'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
index f371e0410bfdf3f3aa2d58e5d8ec56a31d72e4d4..e06dcde6f5c4032b04c4e5b0d2dc222be516768b 100644 (file)
@@ -191,6 +191,11 @@ header .search-box {
       @include lightDark(color, #000, #fff);
       text-decoration: none;
     }
+    &:focus {
+      @include lightDark(background-color, #eee, #333);
+      outline-color: var(--color-primary);
+      color: var(--color-primary);
+    }
   }
   header .dropdown-container {
     display: block;
@@ -225,7 +230,7 @@ header .search-box {
   &:first-child {
     border-inline-end: 1px solid #DDD;
   }
-  &.active {
+  &[aria-selected="true"] {
     border-bottom-color: currentColor;
   }
 }
index 29e4acee7bf8355f7bbda9b17f240e1483cf001f..66604345f3bfb0c4136138eb6269b116dc1ea7da 100644 (file)
@@ -31,7 +31,7 @@
     @include('partials.notifications')
     @include('common.header')
 
-    <div id="content" class="block">
+    <div id="content" components="@yield('content-components')" class="block">
         @yield('content')
     </div>
 
index 52f6b8cbb94b86e386a0120b926d846cfe115569..4799aba24507ae8bfc397853e616a6d76b8ea4b4 100644 (file)
@@ -1,4 +1,4 @@
-<header id="header" header-mobile-toggle class="primary-background">
+<header id="header" component="header-mobile-toggle" class="primary-background">
     <div class="grid mx-l">
 
         <div>
                     <span class="logo-text">{{ setting('app-name') }}</span>
                 @endif
             </a>
-            <div class="mobile-menu-toggle hide-over-l">@icon('more')</div>
+            <button type="button"
+                    refs="header-mobile-toggle@toggle"
+                    title="{{ trans('common.header_menu_expand') }}"
+                    aria-expanded="false"
+                    class="mobile-menu-toggle hide-over-l">@icon('more')</button>
         </div>
 
         <div class="flex-container-row justify-center hide-under-l">
@@ -25,7 +29,7 @@
         </div>
 
         <div class="text-right">
-            <nav class="header-links">
+            <nav refs="header-mobile-toggle@menu" class="header-links">
                 <div class="links text-center">
                     @if (hasAppAccess())
                         <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
@@ -50,7 +54,7 @@
                 </div>
                 @if(signedInUser())
                     <?php $currentUser = user(); ?>
-                    <div class="dropdown-container" component="dropdown">
+                    <div class="dropdown-container" component="dropdown" option:dropdown:bubble-escapes="true">
                         <span class="user-name py-s hide-under-l" refs="dropdown@toggle"
                               aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0">
                             <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
index 71c546964ef6fb31d89a553256654324998de35b..dc4115f291f1a874aa794cb7525a89a6db6fd39d 100644 (file)
@@ -1,21 +1,31 @@
 @extends('base')
 
 @section('body-class', 'tri-layout')
+@section('content-components', 'tri-layout')
 
 @section('content')
 
-    <div class="tri-layout-mobile-tabs text-primary print-hidden">
+    <div class="tri-layout-mobile-tabs print-hidden">
         <div class="grid half no-break no-gap">
-            <div class="tri-layout-mobile-tab px-m py-s" tri-layout-mobile-tab="info">
+            <button type="button"
+                    refs="tri-layout@tab"
+                    data-tab="info"
+                    aria-label="{{ trans('common.tab_info_label') }}"
+                    class="tri-layout-mobile-tab px-m py-m text-primary">
                 {{ trans('common.tab_info') }}
-            </div>
-            <div class="tri-layout-mobile-tab px-m py-s active" tri-layout-mobile-tab="content">
+            </button>
+            <button type="button"
+                    refs="tri-layout@tab"
+                    data-tab="content"
+                    aria-label="{{ trans('common.tab_content_label') }}"
+                    aria-selected="true"
+                    class="tri-layout-mobile-tab px-m py-m text-primary active">
                 {{ trans('common.tab_content') }}
-            </div>
+            </button>
         </div>
     </div>
 
-    <div class="tri-layout-container" tri-layout @yield('container-attrs') >
+    <div refs="tri-layout@container" class="tri-layout-container" @yield('container-attrs') >
 
         <div class="tri-layout-left print-hidden pt-m" id="sidebar">
             <aside class="tri-layout-left-contents">