From 7949fcdc683d07689bad5da5d20bfa3eeb5a6a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 5 Sep 2023 01:25:00 +0200 Subject: Move frontend code to subdirectories, to simplify command line flags. --- static/ts/components/tab-group-element.ts | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 static/ts/components/tab-group-element.ts (limited to 'static/ts/components/tab-group-element.ts') diff --git a/static/ts/components/tab-group-element.ts b/static/ts/components/tab-group-element.ts new file mode 100644 index 00000000..e90997e9 --- /dev/null +++ b/static/ts/components/tab-group-element.ts @@ -0,0 +1,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 + + +*/ +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; + } + +} -- cgit v1.2.3 From f653a01328be3b8be6af35c0c96867623765ca5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 5 Sep 2023 11:41:46 +0200 Subject: Move JS documentation into the JS-code. Texinfo was a bad match for how TypeScript is structured. This also allows generation of jsdoc pages, which can be nice. Another large win is that this opens up for the texinfo pages to replace the Guile heading with different subheadings, including - external library - internal library - C library - ... --- static/ts/components/tab-group-element.ts | 49 ++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) (limited to 'static/ts/components/tab-group-element.ts') diff --git a/static/ts/components/tab-group-element.ts b/static/ts/components/tab-group-element.ts index e90997e9..ce532cec 100644 --- a/static/ts/components/tab-group-element.ts +++ b/static/ts/components/tab-group-element.ts @@ -1,3 +1,40 @@ +/** + * `` + +A group of tabs, where only one can be visible at a time. + +@privateRemarks TODO which form does the HTML document have? For CSS purposes + +Each tab consists of two parts, a label which is used for selecting +it, and a tab-element, which contains the actual content. These two +should refer to each other as follows: + +@example +``` ++---------------+ +-----------------+ +| TabLabel | | Tab | ++---------------+ +-----------------+ +| id |<----| aria-labelledby | +| aria-controls |---->| id | ++---------------+ +-----------------+ +``` + +Further information about tabs in HTML can be found here: +https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role + +#### CSS Variables + +##### tabcount +Each tab element has the style property `--tabcount` set to how +many tabs it has. This is mostly useful to make sure the tab context +is large enough to fit all tab labels without overflowing. + + * + * @category Web Components + * @mergeTarget components + * @module + */ + import { ComponentVEvent } from './vevent' import { makeElement, gensym } from '../lib' import { EditRRule } from './edit-rrule' @@ -6,7 +43,7 @@ import { vcal_objects } from '../globals' export { TabGroupElement } -/* Lacks a template, since it's trivial +/** 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. @@ -81,6 +118,12 @@ class TabGroupElement extends ComponentVEvent { } /* end connectedCallback */ + /** + Adds a new tab to the group. The first parameter will make up the body + of the tab. The label is whath should be shown in the tab selector, + but defaults to the first letter of the text content of the body node. + Title is the hoover text of the label. + */ addTab(child: HTMLElement, label?: string, title?: string) { /* First character of text is a good a guess as any for our label, @@ -128,6 +171,10 @@ class TabGroupElement extends ComponentVEvent { this.style.setProperty('--tabcount', '' + this.tabs.length); } + /** + HTMLElement must be one of the tab bodies in this group. This method + removes it, along with its TabLabel. + */ removeTab(tab: HTMLElement) { let id = tab.getAttribute('aria-labelledby')! let label = document.getElementById(id) -- cgit v1.2.3 From e753d721519f72014241b3d2fc804a919f655769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 7 Sep 2023 02:58:41 +0200 Subject: Document remaining javascript items. --- static/ts/components/tab-group-element.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'static/ts/components/tab-group-element.ts') diff --git a/static/ts/components/tab-group-element.ts b/static/ts/components/tab-group-element.ts index ce532cec..bcd45b40 100644 --- a/static/ts/components/tab-group-element.ts +++ b/static/ts/components/tab-group-element.ts @@ -58,9 +58,12 @@ export { TabGroupElement } */ class TabGroupElement extends ComponentVEvent { + /** The container holding all the tabLabels */ readonly menu: HTMLElement; + /** Contents of each tab */ tabs: HTMLElement[] = []; + /** Label element of each tab */ tabLabels: HTMLElement[] = []; constructor(uid?: string) { @@ -166,7 +169,7 @@ class TabGroupElement extends ComponentVEvent { this.tabLabels.push(tabLabel); this.menu.appendChild(tabLabel); - tabLabel.addEventListener('click', () => this.tabClickedCallback(tabLabel)); + tabLabel.addEventListener('click', () => this.#tabClickedCallback(tabLabel)); this.style.setProperty('--tabcount', '' + this.tabs.length); } @@ -199,7 +202,7 @@ class TabGroupElement extends ComponentVEvent { } /* TODO replace querySelectors here with our already saved references */ - tabClickedCallback(tab: Element) { + #tabClickedCallback(tab: Element) { /* hide all tab panels */ for (let tabcontent of this.querySelectorAll('[role="tabpanel"]')) { @@ -218,7 +221,7 @@ class TabGroupElement extends ComponentVEvent { } - /* returns our rrule tab if we have one */ + /** Return our rrule tab if we have one */ has_rrule_tab(): Element | false { for (let child of this.children) { if (child.firstChild! instanceof EditRRule) { -- cgit v1.2.3