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
|
/// a tabs library.
//@deno-types=./19.ts
import { $, $$, on, attr, next, prev, asHtml, hotkey, behavior, makelogger, identify, dispatch } from "./19.js";
const ilog = makelogger("tabs");
/**
* @param {Element} tablist
* @returns {HTMLElement[]}
*/
const tabsOf = tablist => $$(tablist, "[role=tab]");
/**
* @param {Element} tablist
* @returns {HTMLElement | null}
*/
const currentTab = tablist => $(tablist, "[role=tab][aria-selected=true]");
/**
* @param {Element} tab
* @param {import("./19.ts").Root} root
* @returns {HTMLElement | null}
*/
const tabPanelOf = (tab, root) => {
const id = attr(tab, "aria-controls");
if (id === null) return console.error("Tab", tab, "has no associated tabpanel"), null;
return root.getElementById(id);
}
/**
* @param {import("./19.ts").Root} root
* @param {Element} tablist
* @param {HTMLElement | null} tab
* @returns {void}
*/
const switchTab = (root, tablist, tab, { focusTab = true } = {}) => {
if (!tab) return;
const curtab = currentTab(tablist);
if (curtab) {
attr(curtab, { ariaSelected: false, tabindex: -1 });
const tabpanel = tabPanelOf(curtab, root);
if (tabpanel) tabpanel.hidden = true;
}
attr(tab, { ariaSelected: true, tabindex: 0 });
const tabpanel = tabPanelOf(tab, root);
if (tabpanel) tabpanel.hidden = false;
if (focusTab) tab.focus();
tablist.tabIndex = -1;
dispatch(curtab, "missing-switch-away", { to: tab })
dispatch(tab, "missing-switch-to", { from: curtab })
dispatch(tablist, "missing-change", { from: curtab, to: tab })
};
/**
* https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/
*/
export const tablist = behavior("[role=tablist]", (tablist, { root }) => {
if (!(tablist instanceof HTMLElement)) return;
tablist.tabIndex = 0;
tabsOf(tablist).forEach(tab => {
tab.tabIndex = -1;
tabPanelOf(tab, root).setAttribute("aria-labelledby", identify(tab));
});
if (!(tablist.hasAttribute("aria-labelledby") || tablist.hasAttribute("aria-label")))
console.error("Tab list", tablist, "has no accessible name (aria-label or aria-labelledby)");
switchTab(root, tablist, currentTab(tablist), { focusTab: false });
on(tablist, "focus", _ => currentTab(tablist)?.focus());
on(tablist, "click", e => switchTab(root, tablist, asHtml(asHtml(e.target)?.closest("[role=tab]"))));
on(tablist, "focusin", e => switchTab(root, tablist, asHtml(asHtml(e.target)?.closest("[role=tab]"))));
on(tablist, "keydown", hotkey({
"ArrowRight": e => asHtml(next(tablist, "[role=tab]", asHtml(e.target)))?.focus(),
"ArrowLeft": e => asHtml(prev(tablist, "[role=tab]", asHtml(e.target)))?.focus(),
"Home": _ => tabsOf(tablist).at(0)?.focus(),
"End": _ => tabsOf(tablist).at(-1)?.focus(),
}));
})
tablist(document);
export default tablist;
|