aboutsummaryrefslogtreecommitdiff
path: root/static/components/tab-group-element.ts
blob: e90997e9794ff51d573bbb06643ce7cdf8729a22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import { ComponentVEvent } from './vevent'
import { makeElement, gensym } from '../lib'
import { EditRRule } from './edit-rrule'
import { VEvent, isRedrawable } from '../vevent'
import { vcal_objects } from '../globals'

export { TabGroupElement }

/* Lacks a template, since it's trivial
   The initial children of this element all becomes tabs, each child may have
   the datapropertys 'label' and 'title' set, where label is what is shown in
   the tab bar, and title is the hower text.

   All additions and removals of tabs MUST go through addTab and removeTab!

   Information about how tabs should work from an accesability standpoint can be
   found here:
    https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role

   <tab-group/>
*/
class TabGroupElement extends ComponentVEvent {

    readonly menu: HTMLElement;

    tabs: HTMLElement[] = [];
    tabLabels: HTMLElement[] = [];

    constructor(uid?: string) {
        super(uid);

        this.menu = makeElement('div', {}, {
            role: 'tablist',
            'aria-label': 'Simple Tabs',
        })
    }

    redraw(data: VEvent) {
        /* Update our tabset to match data:s having or not having of rrule,
           but do nothing if we already match */
        let rrule_tab = this.has_rrule_tab()
        if (data.getProperty('rrule')) {
            if (!this.has_rrule_tab()) {
                /* Note that EditRRule register itself to be updated on changes
                to the event */
                this.addTab(new EditRRule(data.getProperty('uid')),
                    "↺", "Upprepningar");
            }
        } else {
            if (rrule_tab) this.removeTab(rrule_tab as HTMLElement);
        }

        /* TODO is there any case where we want to propagate the draw to any of
           our tabs? or are all our tabs independent? */
    }

    connectedCallback() {
        /* All pre-added children should become tabs, but start with removing
        them and storing them for later */
        let originalChildren: HTMLElement[] = [];
        while (this.firstChild) {
            originalChildren.push(this.removeChild(this.firstChild) as HTMLElement);
        }

        /* Add our tab label menu */
        this.appendChild(this.menu);

        /* Re-add our initial children, but as proper tab elements */
        for (let child of originalChildren) {
            this.addTab(child);
        }

        /* redraw might add or remove tabs depending on our data, so call it here */
        this.redraw(vcal_objects.get(this.uid)!);

        /* All tabs should now be ready, focus the first one */
        if (this.tabLabels.length > 0) {
            this.tabLabels[0].setAttribute('tabindex', '0');
            this.tabLabels[0].click();
        }

    } /* end connectedCallback */

    addTab(child: HTMLElement, label?: string, title?: string) {

        /* First character of text is a good a guess as any for our label,
        but still defaut to '?' if no text is found */
        label = label || child.dataset.label || (child.textContent + '?')[0];
        title = title || child.dataset.title || '';
        let extra_attributes = {};
        /* Used to target a tab by name */
        if (child.dataset.originaltitle) {
            extra_attributes = { 'data-originaltitle': child.dataset.originaltitle }
        }

        let tab_id = gensym('tab_content_');
        let label_id = gensym('tab_label_');

        let tabLabel = makeElement('button', {
            textContent: label,
        }, {
            role: 'tab',
            id: label_id,
            tabindex: -1,
            title: title,
            'aria-selected': false,
            'aria-controls': tab_id,
            ...extra_attributes,
        })

        let tabContainer = makeElement('div', {}, {
            id: tab_id,
            role: 'tabpanel',
            tabindex: 0,
            hidden: 'hidden',
            'aria-labelledby': label_id,
        })

        tabContainer.replaceChildren(child);
        this.tabs.push(tabContainer);
        this.appendChild(tabContainer);

        this.tabLabels.push(tabLabel);
        this.menu.appendChild(tabLabel);

        tabLabel.addEventListener('click', () => this.tabClickedCallback(tabLabel));

        this.style.setProperty('--tabcount', '' + this.tabs.length);
    }

    removeTab(tab: HTMLElement) {
        let id = tab.getAttribute('aria-labelledby')!
        let label = document.getElementById(id)
        if (label) {
            if (label.ariaSelected === 'true') {
                this.tabLabels[0].click();
            }
            this.tabLabels = this.tabLabels.filter(el => el !== label)
            label.remove();
        }
        /* remove tab */
        this.tabs = this.tabs.filter(el => el !== tab)
        this.removeChild(tab);
        if (tab.firstChild) {
            let child = tab.firstChild as HTMLElement;
            if (isRedrawable(child)) {
                vcal_objects.get(this.uid)?.unregister(child)
            }
        }

        this.style.setProperty('--tabcount', '' + this.tabs.length);
    }

    /* TODO replace querySelectors here with our already saved references */
    tabClickedCallback(tab: Element) {

        /* hide all tab panels */
        for (let tabcontent of this.querySelectorAll('[role="tabpanel"]')) {
            tabcontent.setAttribute('hidden', 'hidden');
        }
        /* unselect all (selected) tab handles */
        for (let item of this.querySelectorAll('[aria-selected="true"]')) {
            item.setAttribute('aria-selected', 'false');
        }
        /* re-select ourselves */
        tab.setAttribute('aria-selected', 'true');

        /* unhide our target tab */
        this.querySelector('#' + tab.getAttribute('aria-controls'))!
            .removeAttribute('hidden')
    }


    /* returns our rrule tab if we have one */
    has_rrule_tab(): Element | false {
        for (let child of this.children) {
            if (child.firstChild! instanceof EditRRule) {
                return child;
            }
        }
        return false;
    }

}