]> BookStack Code Mirror - bookstack/commitdiff
Updated generic tab styles and js to force accessible usage
authorDan Brown <redacted>
Sat, 28 Jan 2023 12:50:51 +0000 (12:50 +0000)
committerDan Brown <redacted>
Sat, 28 Jan 2023 12:50:51 +0000 (12:50 +0000)
Added use of more accessible tags to create tabbed-interfaces then
updated css and JS to require use of those attributes rather than custom
techniques.

Updated relevant parts of app.
Some custom parts using their own tabs though, something to improve in
future.

resources/js/components/attachments.js
resources/js/components/image-manager.js
resources/js/components/tabs.js
resources/sass/_components.scss
resources/views/attachments/manager.blade.php
resources/views/pages/parts/image-manager.blade.php
resources/views/settings/customization.blade.php

index b4e400aeb716b53047b0d7c80492e2a9dbdba7b0..d8a506270dfcb3c8764364db961d921a9300c24a 100644 (file)
@@ -45,7 +45,7 @@ export class Attachments extends Component {
         this.stopEdit();
         /** @var {Tabs} */
         const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
-        tabs.show('items');
+        tabs.show('attachment-panel-items');
         window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
             this.list.innerHTML = resp.data;
             window.$components.init(this.list);
index a44fffc1b437776af3d723e445eef617db7757b9..418b7c98a2be0f16801f7b0ac7e45536c6ff8faa 100644 (file)
@@ -140,10 +140,9 @@ export class ImageManager extends Component {
     }
 
     setActiveFilterTab(filterName) {
-        this.filterTabs.forEach(t => t.classList.remove('selected'));
-        const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName);
-        if (activeTab) {
-            activeTab.classList.add('selected');
+        for (const tab of this.filterTabs) {
+            const selected = tab.dataset.filter === filterName;
+            tab.setAttribute('aria-selected', selected ? 'true' : 'false');
         }
     }
 
index 46063d240e3e3704910fe5a0782b6df4bfd73d27..ebab4191c105c97e656880136a4ec64c146ef957 100644 (file)
@@ -1,48 +1,46 @@
-import {onSelect} from "../services/dom";
 import {Component} from "./component";
 
 /**
  * Tabs
- * Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
+ * Uses accessible attributes to drive its functionality.
+ * On tab wrapping element:
+ * - role=tablist
+ * On tabs (Should be a button):
+ * - id
+ * - role=tab
+ * - aria-selected=true/false
+ * - aria-controls=<id-of-panel-section>
+ * On panels:
+ * - id
+ * - tabindex=0
+ * - role=tabpanel
+ * - aria-labelledby=<id-of-tab-for-panel>
+ * - hidden (If not shown by default).
  */
 export class Tabs extends Component {
 
     setup() {
-        this.tabContentsByName = {};
-        this.tabButtonsByName = {};
-        this.allContents = [];
-        this.allButtons = [];
+        this.container = this.$el;
+        this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
+        this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
 
-        for (const [key, elems] of Object.entries(this.$manyRefs || {})) {
-            if (key.startsWith('toggle')) {
-                const cleanKey = key.replace('toggle', '').toLowerCase();
-                onSelect(elems, e => this.show(cleanKey));
-                this.allButtons.push(...elems);
-                this.tabButtonsByName[cleanKey] = elems;
+        this.container.addEventListener('click', event => {
+            const button = event.target.closest('[role="tab"]');
+            if (button) {
+                this.show(button.getAttribute('aria-controls'));
             }
-            if (key.startsWith('content')) {
-                const cleanKey = key.replace('content', '').toLowerCase();
-                this.tabContentsByName[cleanKey] = elems;
-                this.allContents.push(...elems);
-            }
-        }
+        });
     }
 
-    show(key) {
-        this.allContents.forEach(c => {
-            c.classList.add('hidden');
-            c.classList.remove('selected');
-        });
-        this.allButtons.forEach(b => b.classList.remove('selected'));
+    show(sectionId) {
+        for (const panel of this.panels) {
+            panel.toggleAttribute('hidden', panel.id !== sectionId);
+        }
 
-        const contents = this.tabContentsByName[key] || [];
-        const buttons = this.tabButtonsByName[key] || [];
-        if (contents.length > 0) {
-            contents.forEach(c => {
-                c.classList.remove('hidden')
-                c.classList.add('selected')
-            });
-            buttons.forEach(b => b.classList.add('selected'));
+        for (const tab of this.tabs) {
+            const tabSection = tab.getAttribute('aria-controls');
+            const selected = tabSection === sectionId;
+            tab.setAttribute('aria-selected', selected ? 'true' : 'false');
         }
     }
 
index b902220a759fe159289e86e791575c3411a4fc49..c8ecd438dde5e7a4692e91e08fbcfa7da80abedb 100644 (file)
@@ -607,7 +607,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 }
 
 
-.tab-container .nav-tabs {
+.tab-container [role="tablist"] {
   display: flex;
   align-items: end;
   justify-items: start;
@@ -617,26 +617,24 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   margin-bottom: $-m;
 }
 
-.nav-tabs {
-  text-align: center;
-  .tab-item {
-    display: inline-block;
-    padding: $-s;
-    @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5));
-    cursor: pointer;
-    border-bottom: 2px solid transparent;
-    margin-bottom: -1px;
-    &.selected {
-      color: var(--color-primary) !important;
-      border-bottom-color: var(--color-primary) !important;
-    }
-    &:hover, &:focus {
-      @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8));
-      @include lightDark(border-bottom-color,  rgba(0, 0, 0, .2), rgba(255, 255, 255, .2));
-    }
+.tab-container [role="tablist"] button[role="tab"],
+.image-manager [role="tablist"] button[role="tab"] {
+  display: inline-block;
+  padding: $-s;
+  @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5));
+  cursor: pointer;
+  border-bottom: 2px solid transparent;
+  margin-bottom: -1px;
+  &[aria-selected="true"] {
+    color: var(--color-primary) !important;
+    border-bottom-color: var(--color-primary) !important;
+  }
+  &:hover, &:focus {
+    @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8));
+    @include lightDark(border-bottom-color,  rgba(0, 0, 0, .2), rgba(255, 255, 255, .2));
   }
 }
-.nav-tabs.controls-card {
+.tab-container [role="tablist"].controls-card {
   margin-bottom: 0;
   border-bottom: 0;
   padding: 0 $-xs;
index 724ca9c8eb93c38de37c75610aeaf31583732617..7d14d00e7d29600fa9b16e020de1b756e26176d8 100644 (file)
@@ -9,25 +9,54 @@
     <div class="px-l files">
 
         <div refs="attachments@listContainer">
-            <p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
+            <p class="text-muted small">{{ trans('entities.attachments_explain') }} <span
+                        class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
 
             <div component="tabs" refs="attachments@mainTabs" class="tab-container">
-                <div class="nav-tabs">
-                    <button refs="tabs@toggleItems" type="button" class="selected tab-item">{{ trans('entities.attachments_items') }}</button>
-                    <button refs="tabs@toggleUpload" type="button" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
-                    <button refs="tabs@toggleLinks" type="button" class="tab-item">{{ trans('entities.attachments_link') }}</button>
+                <div role="tablist">
+                    <button id="attachment-tab-items"
+                            role="tab"
+                            aria-selected="true"
+                            aria-controls="attachment-panel-items"
+                            type="button"
+                            class="tab-item">{{ trans('entities.attachments_items') }}</button>
+                    <button id="attachment-tab-upload"
+                            role="tab"
+                            aria-selected="false"
+                            aria-controls="attachment-panel-upload"
+                            type="button"
+                            class="tab-item">{{ trans('entities.attachments_upload') }}</button>
+                    <button id="attachment-tab-links"
+                            role="tab"
+                            aria-selected="false"
+                            aria-controls="attachment-panel-links"
+                            type="button"
+                            class="tab-item">{{ trans('entities.attachments_link') }}</button>
                 </div>
-                <div refs="tabs@contentItems attachments@list">
+                <div id="attachment-panel-items"
+                     tabindex="0"
+                     role="tabpanel"
+                     aria-labelledby="attachment-tab-items"
+                     refs="attachments@list">
                     @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
                 </div>
-                <div refs="tabs@contentUpload" class="hidden">
+                <div id="attachment-panel-upload"
+                     tabindex="0"
+                     role="tabpanel"
+                     hidden
+                     aria-labelledby="attachment-tab-upload">
                     @include('form.dropzone', [
                         'placeholder' => trans('entities.attachments_dropzone'),
                         'url' =>  url('/attachments/upload?uploaded_to=' . $page->id),
                         'successMessage' => trans('entities.attachments_file_uploaded'),
                     ])
                 </div>
-                <div refs="tabs@contentLinks" class="hidden link-form-container">
+                <div id="attachment-panel-links"
+                     tabindex="0"
+                     role="tabpanel"
+                     hidden
+                     aria-labelledby="attachment-tab-links"
+                     class="link-form-container">
                     @include('attachments.manager-link-form', ['pageId' => $page->id])
                 </div>
             </div>
index a21a5fdac2d3f3b1076aca3f4959fd4b3c1dffb7..5832c0954fc9374a79fc99e81cbd4f400ac24e78 100644 (file)
             <div class="flex-fill image-manager-body">
 
                 <div class="image-manager-content">
-                    <div class="image-manager-header primary-background-light nav-tabs grid third no-gap">
+                    <div role="tablist" class="image-manager-header primary-background-light grid third no-gap">
                         <button refs="image-manager@filterTabs"
                                 data-filter="all"
-                                type="button" class="tab-item selected" title="{{ trans('components.image_all_title') }}">@icon('images') {{ trans('components.image_all') }}</button>
+                                role="tab"
+                                aria-selected="true"
+                                type="button" class="tab-item" title="{{ trans('components.image_all_title') }}">@icon('images') {{ trans('components.image_all') }}</button>
                         <button refs="image-manager@filterTabs"
                                 data-filter="book"
+                                role="tab"
+                                aria-selected="false"
                                 type="button" class="tab-item" title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon']) {{ trans('entities.book') }}</button>
                         <button refs="image-manager@filterTabs"
                                 data-filter="page"
+                                role="tab"
+                                aria-selected="false"
                                 type="button" class="tab-item" title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon']) {{ trans('entities.page') }}</button>
                     </div>
                     <div>
index d3c20c4b15ae2396ea8a9fdad300cbca85be2cb8..ac1f678a56c32ad7d68c6a2d52e4c54b274fa891 100644 (file)
                     $darkMode = boolval(setting()->getForCurrentUser('dark-mode-enabled'));
                 @endphp
                 <div component="tabs" class="tab-container">
-                    <div class="nav-tabs controls-card">
-                        <button refs="tabs@toggleLight"
-                                type="button"
-                                class="{{ $darkMode ? '' : 'selected' }} tab-item">@icon('light-mode'){{ trans('common.light_mode') }}</button>
-                        <button refs="tabs@toggleDark"
-                                type="button"
-                                class="{{ $darkMode ? 'selected' : '' }} tab-item">@icon('dark-mode'){{ trans('common.dark_mode') }}</button>
+                    <div role="tablist" class="controls-card">
+                        <button type="button"
+                                role="tab"
+                                id="color-scheme-tab-light"
+                                aria-selected="{{ $darkMode ? 'false' : 'true' }}"
+                                aria-controls="color-scheme-panel-light">@icon('light-mode'){{ trans('common.light_mode') }}</button>
+                        <button type="button"
+                                role="tab"
+                                id="color-scheme-tab-dark"
+                                aria-selected="{{ $darkMode ? 'true' : 'false' }}"
+                                aria-controls="color-scheme-panel-dark">@icon('dark-mode'){{ trans('common.dark_mode') }}</button>
                     </div>
                     <div class="sub-card">
-                        <div refs="tabs@contentLight attachments@list" class="{{ $darkMode ? 'hidden' : '' }} p-m">
+                        <div id="color-scheme-panel-light"
+                             tabindex="0"
+                             role="tabpanel"
+                             aria-labelledby="color-scheme-tab-light"
+                             @if($darkMode) hidden @endif
+                             class="p-m">
                             @include('settings.parts.setting-color-scheme', ['mode' => 'light'])
                         </div>
-                        <div refs="tabs@contentDark" class="{{ $darkMode ? '' : 'hidden' }} p-m">
+                        <div id="color-scheme-panel-dark"
+                             tabindex="0"
+                             role="tabpanel"
+                             aria-labelledby="color-scheme-tab-light"
+                             @if(!$darkMode) hidden @endif
+                             class="p-m">
                             @include('settings.parts.setting-color-scheme', ['mode' => 'dark'])
                         </div>
                     </div>