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);
}
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');
}
}
-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');
}
}
}
-.tab-container .nav-tabs {
+.tab-container [role="tablist"] {
display: flex;
align-items: end;
justify-items: start;
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;
<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>
<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>
$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>