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();
/** @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);

View 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');
}
}

View 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'));
}
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'));
show(sectionId) {
for (const panel of this.panels) {
panel.toggleAttribute('hidden', panel.id !== sectionId);
}
for (const tab of this.tabs) {
const tabSection = tab.getAttribute('aria-controls');
const selected = tabSection === sectionId;
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
}
}

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;
align-items: end;
justify-items: start;
@ -617,16 +617,15 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
margin-bottom: $-m;
}
.nav-tabs {
text-align: center;
.tab-item {
.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;
&.selected {
&[aria-selected="true"] {
color: var(--color-primary) !important;
border-bottom-color: var(--color-primary) !important;
}
@ -635,8 +634,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
@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;

View 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>

View File

@ -15,15 +15,21 @@
<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>

View File

@ -80,19 +80,33 @@
$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>