]> BookStack Code Mirror - bookstack/blob - resources/js/components/tabs.ts
Comments: Moved to tab UI, Converted tabs component to ts
[bookstack] / resources / js / components / tabs.ts
1 import {Component} from './component';
2
3 /**
4  * Tabs
5  * Uses accessible attributes to drive its functionality.
6  * On tab wrapping element:
7  * - role=tablist
8  * On tabs (Should be a button):
9  * - id
10  * - role=tab
11  * - aria-selected=true/false
12  * - aria-controls=<id-of-panel-section>
13  * On panels:
14  * - id
15  * - tabindex=0
16  * - role=tabpanel
17  * - aria-labelledby=<id-of-tab-for-panel>
18  * - hidden (If not shown by default).
19  */
20 export class Tabs extends Component {
21
22     protected container: HTMLElement;
23     protected tabList: HTMLElement;
24     protected tabs: HTMLElement[];
25     protected panels: HTMLElement[];
26
27     protected activeUnder: number;
28     protected active: null|boolean = null;
29
30     setup() {
31         this.container = this.$el;
32         this.tabList = this.container.querySelector('[role="tablist"]') as HTMLElement;
33         this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
34         this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
35         this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
36
37         this.container.addEventListener('click', event => {
38             const tab = (event.target as HTMLElement).closest('[role="tab"]');
39             if (tab instanceof HTMLElement && this.tabs.includes(tab)) {
40                 this.show(tab.getAttribute('aria-controls') || '');
41             }
42         });
43
44         window.addEventListener('resize', this.updateActiveState.bind(this), {
45             passive: true,
46         });
47         this.updateActiveState();
48     }
49
50     public show(sectionId: string): void {
51         for (const panel of this.panels) {
52             panel.toggleAttribute('hidden', panel.id !== sectionId);
53         }
54
55         for (const tab of this.tabs) {
56             const tabSection = tab.getAttribute('aria-controls');
57             const selected = tabSection === sectionId;
58             tab.setAttribute('aria-selected', selected ? 'true' : 'false');
59         }
60
61         this.$emit('change', {showing: sectionId});
62     }
63
64     protected updateActiveState(): void {
65         const active = window.innerWidth < this.activeUnder;
66         if (active === this.active) {
67             return;
68         }
69
70         if (active) {
71             this.activate();
72         } else {
73             this.deactivate();
74         }
75
76         this.active = active;
77     }
78
79     protected activate(): void {
80         const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];
81         this.show(panelToShow.id);
82         this.tabList.toggleAttribute('hidden', false);
83     }
84
85     protected deactivate(): void {
86         for (const panel of this.panels) {
87             panel.removeAttribute('hidden');
88         }
89         for (const tab of this.tabs) {
90             tab.setAttribute('aria-selected', 'false');
91         }
92         this.tabList.toggleAttribute('hidden', true);
93     }
94
95 }