drab 7.0.1 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/announcer/define.d.ts +1 -0
  2. package/dist/announcer/define.d.ts.map +1 -0
  3. package/dist/announcer/index.d.ts +1 -0
  4. package/dist/announcer/index.d.ts.map +1 -0
  5. package/dist/base/index.d.ts +1 -0
  6. package/dist/base/index.d.ts.map +1 -0
  7. package/dist/contextmenu/define.d.ts +1 -0
  8. package/dist/contextmenu/define.d.ts.map +1 -0
  9. package/dist/contextmenu/index.d.ts +2 -1
  10. package/dist/contextmenu/index.d.ts.map +1 -0
  11. package/dist/define.d.ts +1 -0
  12. package/dist/define.d.ts.map +1 -0
  13. package/dist/dialog/define.d.ts +1 -0
  14. package/dist/dialog/define.d.ts.map +1 -0
  15. package/dist/dialog/index.d.ts +2 -1
  16. package/dist/dialog/index.d.ts.map +1 -0
  17. package/dist/editor/define.d.ts +1 -0
  18. package/dist/editor/define.d.ts.map +1 -0
  19. package/dist/editor/index.d.ts +2 -1
  20. package/dist/editor/index.d.ts.map +1 -0
  21. package/dist/fullscreen/define.d.ts +1 -0
  22. package/dist/fullscreen/define.d.ts.map +1 -0
  23. package/dist/fullscreen/index.d.ts +2 -1
  24. package/dist/fullscreen/index.d.ts.map +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/intersect/define.d.ts +1 -0
  28. package/dist/intersect/define.d.ts.map +1 -0
  29. package/dist/intersect/index.d.ts +2 -1
  30. package/dist/intersect/index.d.ts.map +1 -0
  31. package/dist/prefetch/define.d.ts +1 -0
  32. package/dist/prefetch/define.d.ts.map +1 -0
  33. package/dist/prefetch/index.d.ts +1 -0
  34. package/dist/prefetch/index.d.ts.map +1 -0
  35. package/dist/share/define.d.ts +1 -0
  36. package/dist/share/define.d.ts.map +1 -0
  37. package/dist/share/index.d.ts +2 -1
  38. package/dist/share/index.d.ts.map +1 -0
  39. package/dist/tablesort/define.d.ts +1 -0
  40. package/dist/tablesort/define.d.ts.map +1 -0
  41. package/dist/tablesort/index.d.ts +2 -1
  42. package/dist/tablesort/index.d.ts.map +1 -0
  43. package/dist/tabs/define.d.ts +1 -0
  44. package/dist/tabs/define.d.ts.map +1 -0
  45. package/dist/tabs/index.d.ts +1 -0
  46. package/dist/tabs/index.d.ts.map +1 -0
  47. package/dist/types/index.d.ts +1 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/util/define.d.ts +1 -0
  50. package/dist/util/define.d.ts.map +1 -0
  51. package/dist/util/validate.d.ts +1 -0
  52. package/dist/util/validate.d.ts.map +1 -0
  53. package/dist/wakelock/define.d.ts +1 -0
  54. package/dist/wakelock/define.d.ts.map +1 -0
  55. package/dist/wakelock/index.d.ts +2 -1
  56. package/dist/wakelock/index.d.ts.map +1 -0
  57. package/package.json +4 -2
  58. package/src/announcer/define.ts +4 -0
  59. package/src/announcer/index.ts +93 -0
  60. package/src/base/index.ts +290 -0
  61. package/src/contextmenu/define.ts +4 -0
  62. package/src/contextmenu/index.ts +95 -0
  63. package/src/define.ts +11 -0
  64. package/src/dialog/define.ts +4 -0
  65. package/src/dialog/index.ts +120 -0
  66. package/src/editor/define.ts +4 -0
  67. package/src/editor/index.ts +448 -0
  68. package/src/fullscreen/define.ts +4 -0
  69. package/src/fullscreen/index.ts +59 -0
  70. package/src/index.ts +11 -0
  71. package/src/intersect/define.ts +4 -0
  72. package/src/intersect/index.ts +79 -0
  73. package/src/prefetch/define.ts +4 -0
  74. package/src/prefetch/index.ts +195 -0
  75. package/src/share/define.ts +4 -0
  76. package/src/share/index.ts +99 -0
  77. package/src/tablesort/define.ts +4 -0
  78. package/src/tablesort/index.ts +168 -0
  79. package/src/tabs/define.ts +4 -0
  80. package/src/tabs/index.ts +173 -0
  81. package/src/types/index.ts +39 -0
  82. package/src/util/define.ts +10 -0
  83. package/src/util/validate.ts +16 -0
  84. package/src/wakelock/define.ts +4 -0
  85. package/src/wakelock/index.ts +133 -0
@@ -0,0 +1,195 @@
1
+ import { Lifecycle, Trigger, type TriggerAttributes } from "../base/index.js";
2
+
3
+ type Strategy = "hover" | "load" | "visible";
4
+
5
+ export interface PrefetchAttributes extends TriggerAttributes {
6
+ /** When to prefetch the url. */
7
+ strategy?: Strategy;
8
+
9
+ /** Prerender on the client with the Speculation Rules API. */
10
+ prerender?: boolean;
11
+
12
+ /** URL to prefetch. */
13
+ url?: string;
14
+ }
15
+
16
+ // https://developer.chrome.com/blog/speculation-rules-improvements
17
+ type SpeculationRules = {
18
+ prerender?: DocumentRule[] | ListRule[];
19
+ prefetch?: DocumentRule[] | ListRule[];
20
+ };
21
+
22
+ type Rule = {
23
+ expects_no_vary_search?: string;
24
+ referrer_policy?: ReferrerPolicy;
25
+ requires?: string[];
26
+ };
27
+
28
+ type DocumentRule = {
29
+ source: "document";
30
+ where: WhereCondition;
31
+ eagerness?: "immediate" | "moderate" | "eager" | "conservative";
32
+ } & Rule;
33
+
34
+ type ListRule = { source: "list"; urls: string[] } & Rule;
35
+
36
+ type WhereCondition =
37
+ | { href_matches: string }
38
+ | { selector_matches: string }
39
+ | { and: WhereCondition[] }
40
+ | { not: WhereCondition }
41
+ | { or: WhereCondition[] };
42
+
43
+ /**
44
+ * The `Prefetch` element can prefetch a url, or enhance the `HTMLAnchorElement` by loading the HTML for a page before it is navigated to. This element speeds up the navigation for multi-page applications (MPAs).
45
+ *
46
+ * If you are using a framework that already has a prefetch feature or uses a client side router, it is best to use the framework's feature instead of this element to ensure prefetching is working in accordance with the router.
47
+ *
48
+ * ### Attributes
49
+ *
50
+ * `strategy`
51
+ *
52
+ * Set the `strategy` attribute to specify the when the prefetch will take place.
53
+ *
54
+ * - `"hover"` - (default) After `mouseover`, `focus`, or `touchstart` for > 200ms
55
+ * - `"visible"` - Uses an intersection observer to prefetch when the anchor is within the viewport.
56
+ * - `"load"` - Immediately prefetches when the element is loaded, use carefully.
57
+ *
58
+ * `prerender`
59
+ *
60
+ * Use the `prerender` attribute to use the Speculation Rules API when supported to prerender on the client. This allows you to run client side JavaScript in advance instead of only fetching the HTML.
61
+ *
62
+ * Browsers that do not support will still use `<link rel="prefetch">` instead.
63
+ *
64
+ * [Speculation Rules Reference](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)
65
+ *
66
+ * `url`
67
+ *
68
+ * Add a `url` attribute to immediately prefetch a url without having to provide
69
+ * (or in addition to) `trigger` anchor elements.
70
+ *
71
+ * This element can be deprecated once the Speculation Rules API is supported across browsers. The API will be able to prefetch assets in a similar way with the `source: "document"` and `eagerness` features, and will work without JavaScript.
72
+ */
73
+ export class Prefetch extends Lifecycle(Trigger()) {
74
+ #prefetchedUrls = new Set<string>();
75
+
76
+ constructor() {
77
+ super();
78
+ }
79
+
80
+ /** When to prefetch the url. */
81
+ get #strategy() {
82
+ return this.getAttribute("strategy");
83
+ }
84
+
85
+ /** Prerender with the Speculation Rules API. */
86
+ get #prerender() {
87
+ return this.hasAttribute("prerender");
88
+ }
89
+
90
+ /** `url` to prefetch on `mount`. */
91
+ get #url() {
92
+ return this.getAttribute("url");
93
+ }
94
+
95
+ /**
96
+ * Appends `<link rel="prefetch">` or `<script type="speculationrules">`
97
+ * to the head of the document.
98
+ *
99
+ * @param options Configuration options.
100
+ */
101
+ prefetch(options: {
102
+ /** `url` to prefetch. */
103
+ url: string;
104
+
105
+ /**
106
+ * Uses the Speculation Rules API when supported to prerender on the client.
107
+ */
108
+ prerender?: boolean;
109
+ }) {
110
+ const { url } = options;
111
+
112
+ // if not the current page and not already prefetched
113
+ if (!(url === window.location.href) && !this.#prefetchedUrls.has(url)) {
114
+ this.#prefetchedUrls.add(url);
115
+
116
+ if (
117
+ HTMLScriptElement.supports &&
118
+ HTMLScriptElement.supports("speculationrules")
119
+ ) {
120
+ const rules: SpeculationRules = {
121
+ // Currently, adding `prefetch` is required to fallback if `prerender` fails.
122
+ // Possibly will be automatic in the future, in which case it can be removed.
123
+ // https://github.com/WICG/nav-speculation/issues/162#issuecomment-1977818473
124
+ prefetch: [{ source: "list", urls: [url] }],
125
+ };
126
+
127
+ if (options.prerender) rules.prerender = rules.prefetch;
128
+
129
+ const script = document.createElement("script");
130
+ script.type = "speculationrules";
131
+ script.textContent = JSON.stringify(rules);
132
+ document.head.append(script);
133
+ } else {
134
+ const link = document.createElement("link");
135
+ link.rel = "prefetch";
136
+ link.as = "document";
137
+ link.href = url;
138
+ document.head.append(link);
139
+ }
140
+ }
141
+ }
142
+
143
+ override mount() {
144
+ // immediately prefetch the `url` attribute if it exists
145
+ if (this.#url)
146
+ this.prefetch({ url: this.#url, prerender: this.#prerender });
147
+
148
+ // prefetch the `trigger` elements
149
+ const anchors = this.triggers(HTMLAnchorElement);
150
+ const prerender = this.#prerender;
151
+ const strategy = this.#strategy;
152
+
153
+ let prefetchTimer: NodeJS.Timeout;
154
+
155
+ const listener =
156
+ (delay = 200) =>
157
+ (e: Event) => {
158
+ const { href } = e.currentTarget as HTMLAnchorElement;
159
+ prefetchTimer = setTimeout(
160
+ () => this.prefetch({ url: href, prerender }),
161
+ delay,
162
+ );
163
+ };
164
+
165
+ const reset = () => clearTimeout(prefetchTimer);
166
+
167
+ const observer = new IntersectionObserver((entries) => {
168
+ for (const entry of entries) {
169
+ if (entry.isIntersecting) {
170
+ this.prefetch({
171
+ url: (entry.target as HTMLAnchorElement).href,
172
+ prerender,
173
+ });
174
+ }
175
+ }
176
+ });
177
+
178
+ for (const anchor of anchors) {
179
+ if (strategy === "load") {
180
+ this.prefetch({ url: anchor.href, prerender });
181
+ } else if (strategy === "visible") {
182
+ observer.observe(anchor);
183
+ } else {
184
+ // "hover" - default
185
+ anchor.addEventListener("mouseover", listener());
186
+ anchor.addEventListener("mouseout", reset);
187
+ anchor.addEventListener("focus", listener());
188
+ anchor.addEventListener("focusout", reset);
189
+ // immediately append on touchstart, no delay
190
+ // passive: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners
191
+ anchor.addEventListener("touchstart", listener(0), { passive: true });
192
+ }
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { Share } from "./index.js";
3
+
4
+ define("drab-share", Share);
@@ -0,0 +1,99 @@
1
+ import {
2
+ Announce,
3
+ Content,
4
+ type ContentAttributes,
5
+ Lifecycle,
6
+ Trigger,
7
+ type TriggerAttributes,
8
+ } from "../base/index.js";
9
+
10
+ export type ShareAttributes = TriggerAttributes &
11
+ ContentAttributes &
12
+ (
13
+ | {
14
+ /** Share URL */
15
+ url: string;
16
+
17
+ /** `ShareData` text (only supported on some targets) */
18
+ text?: string;
19
+
20
+ /** Share title */
21
+ "share-title"?: string;
22
+ }
23
+ | {
24
+ /** Text to copy */
25
+ text: string;
26
+ }
27
+ );
28
+
29
+ /**
30
+ * Uses the
31
+ * [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
32
+ * to share the `url` if `navigator.share` is supported.
33
+ *
34
+ * Otherwise uses the
35
+ * [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
36
+ * to copy the `url` or `text` provided.
37
+ *
38
+ * ### Attributes
39
+ *
40
+ * `url`
41
+ *
42
+ * URL to share.
43
+ *
44
+ * `text`
45
+ *
46
+ * Text to copy, or the `ShareData` text if `url` is set (only supported on some targets).
47
+ *
48
+ * `share-title`
49
+ *
50
+ * `ShareData` title (only supported on some targets).
51
+ */
52
+ export class Share extends Lifecycle(Trigger(Content(Announce()))) {
53
+ constructor() {
54
+ super();
55
+ }
56
+
57
+ // helper since ShareData expects undefined instead of null
58
+ #attrOrUndefined(name: string) {
59
+ return this.getAttribute(name) ?? undefined;
60
+ }
61
+
62
+ get #title() {
63
+ return this.#attrOrUndefined("share-title");
64
+ }
65
+
66
+ get #text() {
67
+ return this.#attrOrUndefined("text");
68
+ }
69
+
70
+ get #url() {
71
+ return this.#attrOrUndefined("url");
72
+ }
73
+
74
+ override mount() {
75
+ this.listener(() => {
76
+ const data: ShareData = {
77
+ title: this.#title,
78
+ text: this.#text,
79
+ url: this.#url,
80
+ };
81
+
82
+ if (data.url && navigator.canShare && navigator.canShare(data)) {
83
+ return navigator.share(data).catch((e) => {
84
+ // catch abort errors when user cancels the share
85
+ if (!(e instanceof Error) || e.name !== "AbortError") throw e;
86
+ });
87
+ }
88
+
89
+ const copy = data.url || data.text;
90
+
91
+ if (copy) {
92
+ return navigator.clipboard.writeText(copy).then(() => {
93
+ this.announce("copied to clipboard");
94
+ this.swap();
95
+ });
96
+ }
97
+ });
98
+ }
99
+ }
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { TableSort } from "./index.js";
3
+
4
+ define("drab-tablesort", TableSort);
@@ -0,0 +1,168 @@
1
+ import {
2
+ Announce,
3
+ Content,
4
+ type ContentAttributes,
5
+ Lifecycle,
6
+ Trigger,
7
+ type TriggerAttributes,
8
+ } from "../base/index.js";
9
+
10
+ export interface TableSortAttributes
11
+ extends TriggerAttributes,
12
+ ContentAttributes {}
13
+
14
+ export interface TableSortTriggerAttributes {
15
+ "data-type": "string" | "boolean" | "number";
16
+ "data-value": string;
17
+ }
18
+
19
+ /**
20
+ * Wrap a `HTMLTableElement` in the `TableSort` element to have sortable column
21
+ * headers. Set each `th` that you want to sort to the `trigger`. Set the `tbody`
22
+ * element to the `content`.
23
+ *
24
+ * The values of each cell default to the cell's `textContent`. If you would like to
25
+ * provide an alternate value than what appears in the cell to sort by instead,
26
+ * you can set a different value using the `data-value` attribute on the cell.
27
+ *
28
+ * The cells will be sorted as `string` by default. If you want to provide a different
29
+ * datatype `number` or `boolean`, set `data-type="number"` on the corresponding
30
+ * `th`/`trigger` element. The data will be converted to the specified type before sorting.
31
+ */
32
+ export class TableSort extends Lifecycle(Trigger(Content(Announce()))) {
33
+ constructor() {
34
+ super();
35
+ }
36
+
37
+ get #th() {
38
+ return this.triggers(HTMLTableCellElement);
39
+ }
40
+
41
+ /**
42
+ * Removes `data-asc` or `data-desc` from other triggers then sets the correct attribute on the selected trigger.
43
+ *
44
+ * @param trigger
45
+ * @returns `true` if ascending, `false` if descending
46
+ */
47
+ #setAttributes(trigger: HTMLElement) {
48
+ const asc = "data-asc";
49
+ const desc = "data-desc";
50
+
51
+ for (const t of this.triggers(HTMLTableCellElement)) {
52
+ if (t !== trigger) {
53
+ t.removeAttribute(asc);
54
+ t.removeAttribute(desc);
55
+ }
56
+ }
57
+
58
+ if (trigger.hasAttribute(asc)) {
59
+ trigger.removeAttribute(asc);
60
+ trigger.setAttribute(desc, "");
61
+ return false;
62
+ }
63
+
64
+ trigger.removeAttribute(desc);
65
+ trigger.setAttribute(asc, "");
66
+
67
+ return true;
68
+ }
69
+
70
+ override mount() {
71
+ const tbody = this.content(HTMLTableSectionElement);
72
+
73
+ for (const trigger of this.#th) {
74
+ trigger.tabIndex = 0;
75
+ trigger.role = "button";
76
+
77
+ const listener = () => {
78
+ const asc = this.#setAttributes(trigger);
79
+
80
+ Array.from(tbody.querySelectorAll("tr"))
81
+ .sort(comparer(trigger, asc))
82
+ .forEach((tr) => tbody.appendChild(tr));
83
+
84
+ this.announce(
85
+ `sorted table by ${trigger.textContent} in ${asc ? "ascending" : "descending"} order`,
86
+ );
87
+ };
88
+
89
+ trigger.addEventListener(this.event, listener);
90
+
91
+ if (this.event === "click") {
92
+ trigger.addEventListener("keydown", (e) => {
93
+ if (e.key === "Enter" || e.key === " ") {
94
+ e.preventDefault();
95
+ listener();
96
+ }
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ // adapted from: https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
104
+ const comparer = (th: HTMLElement, ascending: boolean) => {
105
+ // this function is returned and used by `sort`
106
+ const sorter = (a: HTMLTableRowElement, b: HTMLTableRowElement) => {
107
+ // find the column to sort by using the index of the `th`
108
+ const columnIndex = Array.from(th.parentNode?.children ?? []).indexOf(th);
109
+
110
+ const compare = (aVal: string, bVal: string) => {
111
+ // default to `string` sorting
112
+ const dataType = (th.dataset.type ?? "string") as
113
+ | "string"
114
+ | "boolean"
115
+ | "number";
116
+
117
+ if (dataType === "string") {
118
+ const collator = new Intl.Collator();
119
+ return collator.compare(aVal, bVal);
120
+ } else if (dataType === "boolean") {
121
+ return falsyBoolean(aVal) === falsyBoolean(bVal)
122
+ ? 0
123
+ : falsyBoolean(aVal)
124
+ ? -1
125
+ : 1;
126
+ } else {
127
+ // "number"
128
+ return Number(aVal) - Number(bVal);
129
+ }
130
+ };
131
+
132
+ return compare(
133
+ getValue(ascending ? a : b, columnIndex),
134
+ getValue(ascending ? b : a, columnIndex),
135
+ );
136
+ };
137
+
138
+ return sorter;
139
+ };
140
+
141
+ /**
142
+ * @param tr the row
143
+ * @param i index of the `td` to find
144
+ * @returns a string, the `data-value` attribute, or the `textContent`
145
+ */
146
+ const getValue = (tr: HTMLTableRowElement, i: number) => {
147
+ const cell = tr.children[i];
148
+ if (cell instanceof HTMLElement) {
149
+ // first look for `data-value` attribute, then use `textContent`
150
+ return cell.dataset.value ?? cell.textContent ?? "";
151
+ }
152
+ return "";
153
+ };
154
+
155
+ /**
156
+ * if value is one of these and type is boolean
157
+ * it should be considered falsy
158
+ * since actually `Boolean("false") === true`
159
+ * @param val string pulled from the textContent or attr
160
+ * @returns a boolean of the provided string
161
+ */
162
+ const falsyBoolean = (val: string) => {
163
+ if (["0", "false", "null", "undefined"].includes(val)) {
164
+ return false;
165
+ }
166
+
167
+ return Boolean(val);
168
+ };
@@ -0,0 +1,4 @@
1
+ import { define } from "../util/define.js";
2
+ import { Tabs } from "./index.js";
3
+
4
+ define("drab-tabs", Tabs);
@@ -0,0 +1,173 @@
1
+ import { Lifecycle, Trigger, type TriggerAttributes } from "../base/index.js";
2
+
3
+ export interface TabsAttributes extends TriggerAttributes {
4
+ /** Set to `"vertical"` if tabs are displayed vertically. */
5
+ orientation?: "horizontal" | "vertical";
6
+ }
7
+
8
+ /**
9
+ * Progressively enhance a list of links and content to be tabs by
10
+ * wrapping with the `Tabs` element. Each `trigger` should be an
11
+ * `HTMLAnchorElement` with the `href` attribute set to the `id` of the
12
+ * corresponding tab panel.
13
+ *
14
+ * > Tip: Set the `height` of the element the `panel`s are contained in with
15
+ * > CSS to prevent layout shift when JS is loaded.
16
+ *
17
+ * This element is based on
18
+ * [Chris Ferdinandi's Toggle Tabs](https://gomakethings.com/a-web-component-ui-library-for-people-who-love-html/#toggle-tabs)
19
+ * design.
20
+ *
21
+ * [ARIA Reference](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
22
+ *
23
+ * - Sets the correct ARIA attributes on each element.
24
+ * - Supports keyboard navigation based on the `orientation` attribute.
25
+ * - First tab is selected by default if no `aria-selected="true"` attribute is
26
+ * found on another tab.
27
+ * - `tablist` is calculated based on the deepest common parent of the tabs,
28
+ * throws an error if not found.
29
+ *
30
+ * ### Attributes
31
+ *
32
+ * `orientation`
33
+ *
34
+ * Set `orientation="vertical"` if the `tablist` element is displayed vertically.
35
+ */
36
+ export class Tabs extends Lifecycle(Trigger()) {
37
+ /** Supported keys for keyboard navigation. */
38
+ #keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
39
+
40
+ /** Currently selected tab. */
41
+ #selected: { tab?: HTMLAnchorElement; index: number } = { index: 0 };
42
+
43
+ constructor() {
44
+ super();
45
+ }
46
+
47
+ /** User provided orientation of the `tablist`. */
48
+ get #orientation() {
49
+ return this.getAttribute("orientation") ?? "horizontal";
50
+ }
51
+
52
+ get #tabs() {
53
+ return this.triggers(HTMLAnchorElement);
54
+ }
55
+
56
+ /**
57
+ * @param tab
58
+ * @returns The ancestors of the tab up to `this`.
59
+ */
60
+ #ancestors(tab?: HTMLElement) {
61
+ const ancestors = new Set<HTMLElement>();
62
+
63
+ let current: HTMLElement | null | undefined = tab;
64
+ while ((current = current?.parentElement) && current !== this) {
65
+ ancestors.add(current);
66
+ }
67
+
68
+ return ancestors;
69
+ }
70
+
71
+ /** A map of each `tab` and its corresponding `panel`. */
72
+ get #map() {
73
+ const map = new Map<HTMLAnchorElement, HTMLElement>();
74
+
75
+ for (const tab of this.#tabs) {
76
+ const panel = this.querySelector(tab.hash);
77
+ if (!(panel instanceof HTMLElement))
78
+ throw new Error(`Tabs: No HTMLElement with ID of ${tab.hash} found.`);
79
+
80
+ map.set(tab, panel);
81
+ }
82
+
83
+ return map;
84
+ }
85
+
86
+ override mount() {
87
+ // create tablist
88
+ const [first, ...rest] = this.#tabs;
89
+ let common = this.#ancestors(first);
90
+ for (let i = 0; i < rest.length; i++) {
91
+ common = common.intersection(this.#ancestors(rest[i]));
92
+ }
93
+ const [tablist] = common;
94
+ if (!tablist)
95
+ throw new Error("Tabs: No common parent element found for triggers.");
96
+ tablist.role = "tablist";
97
+ tablist.ariaOrientation = this.#orientation;
98
+
99
+ // enhance tabs/panels
100
+ let index = 0;
101
+ for (const [tab, panel] of this.#map) {
102
+ tab.role = "tab";
103
+ tab.id = `tab-${panel.id}`;
104
+ tab.setAttribute("aria-controls", panel.id);
105
+ if (tab.ariaSelected) this.#selected = { tab, index };
106
+
107
+ panel.role = "tabpanel";
108
+ panel.setAttribute("aria-labelledby", tab.id);
109
+
110
+ tab.addEventListener(this.event, (e) => {
111
+ e.preventDefault();
112
+
113
+ for (const [t, p] of this.#map) {
114
+ if (t === tab) {
115
+ // show current
116
+ t.ariaSelected = "true";
117
+ t.tabIndex = 0;
118
+ p.hidden = false;
119
+ } else {
120
+ // hide others
121
+ t.ariaSelected = "false";
122
+ t.tabIndex = -1;
123
+ p.hidden = true;
124
+ }
125
+ }
126
+ });
127
+
128
+ index++;
129
+ }
130
+
131
+ // fallback to first
132
+ if (!this.#selected.tab) this.#selected.tab = this.#tabs[0];
133
+
134
+ // select the current tab
135
+ this.#selected.tab?.click();
136
+
137
+ // handle keyboard navigation
138
+ this.addEventListener("keydown", (e) => {
139
+ const i = this.#keys.indexOf(e.key);
140
+ if (i === -1) return;
141
+
142
+ const previousIndex = this.#selected.index;
143
+ const vertical = this.#orientation === "vertical";
144
+
145
+ if (
146
+ ((!vertical && i === 0) || (vertical && i === 1)) &&
147
+ this.#tabs[this.#selected.index + 1]
148
+ ) {
149
+ // next
150
+ this.#selected.tab = this.#tabs[++this.#selected.index];
151
+ } else if (
152
+ ((!vertical && i === 2) || (vertical && i === 3)) &&
153
+ this.#tabs[this.#selected.index - 1]
154
+ ) {
155
+ // previous
156
+ this.#selected.tab = this.#tabs[--this.#selected.index];
157
+ } else if (i === 4) {
158
+ // home
159
+ this.#selected = { tab: this.#tabs[0], index: 0 };
160
+ } else if (i === 5) {
161
+ // end
162
+ const index = this.#tabs.length - 1;
163
+ this.#selected = { tab: this.#tabs[index], index };
164
+ }
165
+
166
+ if (this.#selected.index === previousIndex) return;
167
+
168
+ e.preventDefault();
169
+ this.#selected.tab?.click();
170
+ this.#selected.tab?.focus();
171
+ });
172
+ }
173
+ }
@@ -0,0 +1,39 @@
1
+ import type { AnnouncerAttributes } from "../announcer/index.js";
2
+ import type { ContextMenuAttributes } from "../contextmenu/index.js";
3
+ import type { DialogAttributes } from "../dialog/index.js";
4
+ import type { EditorAttributes } from "../editor/index.js";
5
+ import type { FullscreenAttributes } from "../fullscreen/index.js";
6
+ import type { IntersectAttributes } from "../intersect/index.js";
7
+ import type { PrefetchAttributes } from "../prefetch/index.js";
8
+ import type { ShareAttributes } from "../share/index.js";
9
+ import type { TableSortAttributes } from "../tablesort/index.js";
10
+ import type { TabsAttributes } from "../tabs/index.js";
11
+ import type { WakeLockAttributes } from "../wakelock/index.js";
12
+
13
+ export interface Attributes {
14
+ announcer: AnnouncerAttributes;
15
+ contextmenu: ContextMenuAttributes;
16
+ dialog: DialogAttributes;
17
+ editor: EditorAttributes;
18
+ fullscreen: FullscreenAttributes;
19
+ intersect: IntersectAttributes;
20
+ prefetch: PrefetchAttributes;
21
+ share: ShareAttributes;
22
+ tablesort: TableSortAttributes;
23
+ tabs: TabsAttributes;
24
+ wakelock: WakeLockAttributes;
25
+ }
26
+
27
+ type Prefixed<
28
+ Prefix extends string,
29
+ GlobalAttributes,
30
+ ElementAttributes extends Record<string, any>,
31
+ > = {
32
+ [K in keyof ElementAttributes as `${Prefix}-${Extract<K, string>}`]: ElementAttributes[K] &
33
+ GlobalAttributes;
34
+ };
35
+
36
+ export type Elements<
37
+ GlobalAttributes,
38
+ Prefix extends string = "drab",
39
+ > = Prefixed<Prefix, GlobalAttributes, Attributes>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Define a custom element to the registry. Checks if the element is
3
+ * defined and then names the element.
4
+ *
5
+ * @param name name of the custom element
6
+ * @param Constructor custom element constructor
7
+ */
8
+ export const define = (name: string, Constructor: CustomElementConstructor) => {
9
+ if (!customElements.get(name)) customElements.define(name, Constructor);
10
+ };