Updated generic tab styles and js to force accessible usage

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.
This commit is contained in:
Dan Brown 2023-01-28 12:50:51 +00:00
parent 1f69965c1e
commit e708ce93ba
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 119 additions and 75 deletions

View File

@ -45,7 +45,7 @@ export class Attachments extends Component {
this.stopEdit(); this.stopEdit();
/** @var {Tabs} */ /** @var {Tabs} */
const tabs = window.$components.firstOnElement(this.mainTabs, '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 => { window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data; this.list.innerHTML = resp.data;
window.$components.init(this.list); window.$components.init(this.list);

View File

@ -140,10 +140,9 @@ export class ImageManager extends Component {
} }
setActiveFilterTab(filterName) { setActiveFilterTab(filterName) {
this.filterTabs.forEach(t => t.classList.remove('selected')); for (const tab of this.filterTabs) {
const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName); const selected = tab.dataset.filter === filterName;
if (activeTab) { tab.setAttribute('aria-selected', selected ? 'true' : 'false');
activeTab.classList.add('selected');
} }
} }

View File

@ -1,48 +1,46 @@
import {onSelect} from "../services/dom";
import {Component} from "./component"; import {Component} from "./component";
/** /**
* Tabs * 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 { export class Tabs extends Component {
setup() { setup() {
this.tabContentsByName = {}; this.container = this.$el;
this.tabButtonsByName = {}; this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
this.allContents = []; this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
this.allButtons = [];
for (const [key, elems] of Object.entries(this.$manyRefs || {})) { this.container.addEventListener('click', event => {
if (key.startsWith('toggle')) { const button = event.target.closest('[role="tab"]');
const cleanKey = key.replace('toggle', '').toLowerCase(); if (button) {
onSelect(elems, e => this.show(cleanKey)); this.show(button.getAttribute('aria-controls'));
this.allButtons.push(...elems);
this.tabButtonsByName[cleanKey] = elems;
} }
if (key.startsWith('content')) { });
const cleanKey = key.replace('content', '').toLowerCase();
this.tabContentsByName[cleanKey] = elems;
this.allContents.push(...elems);
}
}
} }
show(key) { show(sectionId) {
this.allContents.forEach(c => { for (const panel of this.panels) {
c.classList.add('hidden'); panel.toggleAttribute('hidden', panel.id !== sectionId);
c.classList.remove('selected'); }
});
this.allButtons.forEach(b => b.classList.remove('selected'));
const contents = this.tabContentsByName[key] || []; for (const tab of this.tabs) {
const buttons = this.tabButtonsByName[key] || []; const tabSection = tab.getAttribute('aria-controls');
if (contents.length > 0) { const selected = tabSection === sectionId;
contents.forEach(c => { tab.setAttribute('aria-selected', selected ? 'true' : 'false');
c.classList.remove('hidden')
c.classList.add('selected')
});
buttons.forEach(b => b.classList.add('selected'));
} }
} }

View 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; display: flex;
align-items: end; align-items: end;
justify-items: start; justify-items: start;
@ -617,26 +617,24 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
margin-bottom: $-m; margin-bottom: $-m;
} }
.nav-tabs { .tab-container [role="tablist"] button[role="tab"],
text-align: center; .image-manager [role="tablist"] button[role="tab"] {
.tab-item { display: inline-block;
display: inline-block; padding: $-s;
padding: $-s; @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5));
@include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); cursor: pointer;
cursor: pointer; border-bottom: 2px solid transparent;
border-bottom: 2px solid transparent; margin-bottom: -1px;
margin-bottom: -1px; &[aria-selected="true"] {
&.selected { color: var(--color-primary) !important;
color: var(--color-primary) !important; border-bottom-color: var(--color-primary) !important;
border-bottom-color: var(--color-primary) !important; }
} &:hover, &:focus {
&:hover, &:focus { @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8));
@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));
@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; margin-bottom: 0;
border-bottom: 0; border-bottom: 0;
padding: 0 $-xs; padding: 0 $-xs;

View File

@ -9,25 +9,54 @@
<div class="px-l files"> <div class="px-l files">
<div refs="attachments@listContainer"> <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 component="tabs" refs="attachments@mainTabs" class="tab-container">
<div class="nav-tabs"> <div role="tablist">
<button refs="tabs@toggleItems" type="button" class="selected tab-item">{{ trans('entities.attachments_items') }}</button> <button id="attachment-tab-items"
<button refs="tabs@toggleUpload" type="button" class="tab-item">{{ trans('entities.attachments_upload') }}</button> role="tab"
<button refs="tabs@toggleLinks" type="button" class="tab-item">{{ trans('entities.attachments_link') }}</button> 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>
<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()]) @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
</div> </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', [ @include('form.dropzone', [
'placeholder' => trans('entities.attachments_dropzone'), 'placeholder' => trans('entities.attachments_dropzone'),
'url' => url('/attachments/upload?uploaded_to=' . $page->id), 'url' => url('/attachments/upload?uploaded_to=' . $page->id),
'successMessage' => trans('entities.attachments_file_uploaded'), 'successMessage' => trans('entities.attachments_file_uploaded'),
]) ])
</div> </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]) @include('attachments.manager-link-form', ['pageId' => $page->id])
</div> </div>
</div> </div>

View File

@ -15,15 +15,21 @@
<div class="flex-fill image-manager-body"> <div class="flex-fill image-manager-body">
<div class="image-manager-content"> <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" <button refs="image-manager@filterTabs"
data-filter="all" 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" <button refs="image-manager@filterTabs"
data-filter="book" 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> 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" <button refs="image-manager@filterTabs"
data-filter="page" 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> type="button" class="tab-item" title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon']) {{ trans('entities.page') }}</button>
</div> </div>
<div> <div>

View File

@ -80,19 +80,33 @@
$darkMode = boolval(setting()->getForCurrentUser('dark-mode-enabled')); $darkMode = boolval(setting()->getForCurrentUser('dark-mode-enabled'));
@endphp @endphp
<div component="tabs" class="tab-container"> <div component="tabs" class="tab-container">
<div class="nav-tabs controls-card"> <div role="tablist" class="controls-card">
<button refs="tabs@toggleLight" <button type="button"
type="button" role="tab"
class="{{ $darkMode ? '' : 'selected' }} tab-item">@icon('light-mode'){{ trans('common.light_mode') }}</button> id="color-scheme-tab-light"
<button refs="tabs@toggleDark" aria-selected="{{ $darkMode ? 'false' : 'true' }}"
type="button" aria-controls="color-scheme-panel-light">@icon('light-mode'){{ trans('common.light_mode') }}</button>
class="{{ $darkMode ? 'selected' : '' }} tab-item">@icon('dark-mode'){{ trans('common.dark_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>
<div class="sub-card"> <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']) @include('settings.parts.setting-color-scheme', ['mode' => 'light'])
</div> </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']) @include('settings.parts.setting-color-scheme', ['mode' => 'dark'])
</div> </div>
</div> </div>