X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/a6633642232efd164d4708967ab59e498fbff896..refs/pull/4913/head:/resources/js/components/tabs.js diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.js index 7121d7044..f0fc058ce 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.js @@ -1,51 +1,88 @@ +import {Component} from './component'; + /** * Tabs - * Works by matching 'tabToggle' with 'tabContent' sections. - * @extends {Component} + * 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= + * On panels: + * - id + * - tabindex=0 + * - role=tabpanel + * - aria-labelledby= + * - hidden (If not shown by default). */ -import {onSelect} from "../services/dom"; - -class Tabs { +export class Tabs extends Component { setup() { - this.tabContentsByName = {}; - this.tabButtonsByName = {}; - this.allContents = []; - this.allButtons = []; - - 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; - } - if (key.startsWith('content')) { - const cleanKey = key.replace('content', '').toLowerCase(); - this.tabContentsByName[cleanKey] = elems; - this.allContents.push(...elems); + this.container = this.$el; + this.tabList = this.container.querySelector('[role="tablist"]'); + this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]')); + this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]')); + this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000; + this.active = null; + + this.container.addEventListener('click', event => { + const tab = event.target.closest('[role="tab"]'); + if (tab && this.tabs.includes(tab)) { + this.show(tab.getAttribute('aria-controls')); } + }); + + window.addEventListener('resize', this.updateActiveState.bind(this), { + passive: true, + }); + this.updateActiveState(); + } + + 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'); + } + + this.$emit('change', {showing: sectionId}); } - 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')); + updateActiveState() { + const active = window.innerWidth < this.activeUnder; + if (active === this.active) { + return; + } + + if (active) { + this.activate(); + } else { + this.deactivate(); } + + this.active = active; } -} + activate() { + const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0]; + this.show(panelToShow.id); + this.tabList.toggleAttribute('hidden', false); + } -export default Tabs; \ No newline at end of file + deactivate() { + for (const panel of this.panels) { + panel.removeAttribute('hidden'); + } + for (const tab of this.tabs) { + tab.setAttribute('aria-selected', 'false'); + } + this.tabList.toggleAttribute('hidden', true); + } + +}