drab 6.2.2 → 6.4.0

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.
@@ -1,26 +1,8 @@
1
1
  import { Announcer } from "../announcer/index.js";
2
- /**
3
- * Each element in the library extends the `Base` class. It provides methods
4
- * for selecting elements via HTML attributes along with other helpers.
5
- *
6
- * By default, `trigger`s and `content` will be selected via the `data-trigger` and
7
- * `data-content` attributes. Alternatively, you can set the `trigger` or
8
- * `content` attribute to a CSS selector to change the default selector from
9
- * `[data-trigger]` or `[data-content]` to a selector of your choosing.
10
- * This can be useful if you have multiple elements within one another.
11
- *
12
- * Each element can have multiple `trigger`s, but will only have one `content`.
13
- */
14
- export class Base extends HTMLElement {
15
- /**
16
- * A single `Announcer` element to share between all drab elements to announce
17
- * interactive changes.
18
- */
19
- static #announcer = Announcer.init();
20
- /** To clean up event listeners added to `document` when the element is removed. */
21
- #listenerController = new AbortController();
22
- constructor() {
23
- super();
2
+ import { validate } from "../util/validate.js";
3
+ export const Trigger = (Super) => class Trigger extends Super {
4
+ constructor(...args) {
5
+ super(args);
24
6
  }
25
7
  /**
26
8
  * Event for the `trigger` to execute.
@@ -30,35 +12,32 @@ export class Base extends HTMLElement {
30
12
  * @default "click"
31
13
  */
32
14
  get event() {
33
- return this.getAttribute("event") ?? "click";
15
+ return (this.getAttribute("event") ?? "click");
34
16
  }
35
17
  set event(value) {
36
18
  this.setAttribute("event", value);
37
19
  }
38
- /**
39
- * @param message message to announce to screen readers
40
- */
41
- announce(message) {
42
- Base.#announcer.announce(message);
20
+ getTrigger(instance = HTMLElement) {
21
+ const triggers = this.querySelectorAll(this.getAttribute("trigger") ?? "[data-trigger]");
22
+ for (const trigger of triggers)
23
+ validate(trigger, instance);
24
+ return triggers;
43
25
  }
44
26
  /**
45
- * @returns All of the elements that match the `trigger` selector.
46
- * @default this.querySelectorAll("[data-trigger]")
27
+ * @param listener Listener to attach to all of the `trigger` elements.
47
28
  */
48
- getTrigger() {
49
- return this.querySelectorAll(this.getAttribute("trigger") ?? "[data-trigger]");
29
+ triggerListener(listener, type = this.event, options) {
30
+ for (const trigger of this.getTrigger()) {
31
+ trigger.addEventListener(type, listener, options);
32
+ }
33
+ }
34
+ };
35
+ export const Content = (Super) => class Content extends Super {
36
+ constructor(...args) {
37
+ super(args);
50
38
  }
51
- /**
52
- * @param instance The instance of the desired element to validate against,
53
- * ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
54
- * @returns The element that matches the `content` selector.
55
- * @default this.querySelector("[data-content]")
56
- */
57
39
  getContent(instance = HTMLElement) {
58
- const content = this.querySelector(this.getAttribute("content") ?? "[data-content]");
59
- if (content instanceof instance)
60
- return content;
61
- throw new Error("Content not found");
40
+ return validate(this.querySelector(this.getAttribute("content") ?? "[data-content]"), instance);
62
41
  }
63
42
  /**
64
43
  * Finds the `HTMLElement | HTMLTemplateElement` via the `swap` selector and
@@ -98,22 +77,26 @@ export class Base extends HTMLElement {
98
77
  }
99
78
  }
100
79
  }
80
+ };
81
+ export const Lifecycle = (Super) => class Lifecycle extends Super {
82
+ /** To clean up event listeners added to `document` when the element is removed. */
83
+ #listenerController = new AbortController();
84
+ constructor(...args) {
85
+ super(args);
86
+ }
101
87
  safeListener(type, listener, target = document.body, options = {}) {
102
88
  options.signal = this.#listenerController.signal;
103
89
  target.addEventListener(type, listener, options);
104
90
  }
105
91
  /**
106
- * @param listener Listener to attach to all of the `trigger` elements.
107
- */
108
- triggerListener(listener, type = this.event, options) {
109
- for (const trigger of this.getTrigger()) {
110
- trigger.addEventListener(type, listener, options);
111
- }
112
- }
113
- /**
114
- * Passed into `queueMicrotask` in `connectedCallback`. It is overridden in each component that needs to run `connectedCallback`.
92
+ * Passed into `queueMicrotask` in `connectedCallback`.
93
+ * It is overridden in each component that needs to run `connectedCallback`.
115
94
  *
116
- * The reason for this is to make these elements work better with frameworks like Svelte. For SSR this isn't necessary, but when client side rendering, the HTML within the custom element isn't available before `connectedCallback` is called. By waiting until the next microtask, the HTML content is available---then for example, listeners can be attached to elements inside.
95
+ * The reason for this is to make these elements work better with frameworks like Svelte.
96
+ * For SSR this isn't necessary, but when client side rendering, the HTML within the
97
+ * custom element isn't available before `connectedCallback` is called. By waiting until
98
+ * the next microtask, the HTML content is available---then for example, listeners can
99
+ * be attached to elements inside.
117
100
  */
118
101
  mount() { }
119
102
  /** Called when custom element is added to the page. */
@@ -129,4 +112,37 @@ export class Base extends HTMLElement {
129
112
  this.destroy();
130
113
  this.#listenerController.abort();
131
114
  }
115
+ };
116
+ export const Announce = (Super) => class Announce extends Super {
117
+ /**
118
+ * A single `Announcer` element to share between all drab elements to announce
119
+ * interactive changes.
120
+ */
121
+ static #announcer = Announcer.init();
122
+ constructor(...args) {
123
+ super(args);
124
+ }
125
+ /**
126
+ * @param message message to announce to screen readers
127
+ */
128
+ announce(message) {
129
+ Announce.#announcer.announce(message);
130
+ }
131
+ };
132
+ /**
133
+ * Each element in the library extends the `Base` class. It provides methods
134
+ * for selecting elements via HTML attributes along with other helpers.
135
+ *
136
+ * By default, `trigger`s and `content` will be selected via the `data-trigger` and
137
+ * `data-content` attributes. Alternatively, you can set the `trigger` or
138
+ * `content` attribute to a CSS selector to change the default selector from
139
+ * `[data-trigger]` or `[data-content]` to a selector of your choosing.
140
+ * This can be useful if you have multiple elements within one another.
141
+ *
142
+ * Each element can have multiple `trigger`s, but will only have one `content`.
143
+ */
144
+ export class Base extends Trigger(Content(Lifecycle(Announce(HTMLElement)))) {
145
+ constructor() {
146
+ super();
147
+ }
132
148
  }
@@ -3,8 +3,15 @@ export type CopyAttributes = BaseAttributes & {
3
3
  value: string;
4
4
  };
5
5
  /**
6
- * Uses the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
6
+ * Uses the
7
+ * [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
7
8
  * to copy text.
9
+ *
10
+ * ### Attributes
11
+ *
12
+ * `value`
13
+ *
14
+ * Text to copy.
8
15
  */
9
16
  export declare class Copy extends Base {
10
17
  constructor();
@@ -1,7 +1,14 @@
1
1
  import { Base } from "../base/index.js";
2
2
  /**
3
- * Uses the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
3
+ * Uses the
4
+ * [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)
4
5
  * to copy text.
6
+ *
7
+ * ### Attributes
8
+ *
9
+ * `value`
10
+ *
11
+ * Text to copy.
5
12
  */
6
13
  export class Copy extends Base {
7
14
  constructor() {
@@ -11,6 +11,8 @@ export type DialogAttributes = BaseAttributes & {
11
11
  * By default, the `HTMLDialogElement` doesn't close if the user clicks outside of it.
12
12
  * Add a `click-outside-close` attribute to close when the user clicks outside.
13
13
  *
14
+ * ### Attributes
15
+ *
14
16
  * `remove-body-scroll`
15
17
  *
16
18
  * Add the `remove-body-scroll` attribute to remove the scroll from `document.body` when the dialog
@@ -7,6 +7,8 @@ import { Base } from "../base/index.js";
7
7
  * By default, the `HTMLDialogElement` doesn't close if the user clicks outside of it.
8
8
  * Add a `click-outside-close` attribute to close when the user clicks outside.
9
9
  *
10
+ * ### Attributes
11
+ *
10
12
  * `remove-body-scroll`
11
13
  *
12
14
  * Add the `remove-body-scroll` attribute to remove the scroll from `document.body` when the dialog
@@ -19,6 +19,23 @@ export type ContentElement = {
19
19
  /**
20
20
  * Enhances the `textarea` element with controls to add content and keyboard shortcuts. Compared to other WYSIWYG editors, the `text` value is just a `string`, so you can easily store it in a database or manipulate it without learning a separate API.
21
21
  *
22
+ * - Automatically adds closing characters for `keyPairs`. For example, when
23
+ * typing `(`, `)` will be inserted and typed over when reached. All content
24
+ * with `data-type="wrap"` is also added to `keyPairs`.
25
+ * - Highlights the first word of the text inserted if it contains letters.
26
+ * - Automatically increments/decrements ordered lists.
27
+ * - Adds the starting character to the next line for `block` content.
28
+ * - On double click, highlight is corrected to only highlight the current word
29
+ * without space around it.
30
+ * - `tab` key will indent or dedent (+shift) instead of focus change if the
31
+ * selection is within a code block (three backticks).
32
+ * - When text is highlighted and a `wrap` character `keyPair` is typed, the
33
+ * highlighted text will be wrapped with the character instead of removing it.
34
+ * For example, if a word is highlighted and the `"` character is typed, the
35
+ * work will be surrounded by `"`s.
36
+ *
37
+ * ### Trigger attributes
38
+ *
22
39
  * `data-value`
23
40
  *
24
41
  * Set the value of the text to be inserted using the `data-value` attribute on the `trigger`.
@@ -35,15 +52,6 @@ export type ContentElement = {
35
52
  *
36
53
  * Add a `ctrl`/`meta` keyboard shortcut for the content based on the `data-key` attribute.
37
54
  *
38
- * Other features:
39
- *
40
- * - Automatically adds closing characters for `keyPairs`. For example, when typing `(`, `)` will be inserted and typed over when reached. All content with `data-type="wrap"` is also added to `keyPairs`.
41
- * - Highlights the first word of the text inserted if it contains letters.
42
- * - Automatically increments/decrements ordered lists.
43
- * - Adds the starting character to the next line for `block` content.
44
- * - On double click, highlight is corrected to only highlight the current word without space around it.
45
- * - `tab` key will indent or dedent (+shift) instead of focus change if the selection is within a code block (three backticks).
46
- * - When text is highlighted and a `wrap` character `keyPair` is typed, the highlighted text will be wrapped with the character instead of removing it. For example, if a word is highlighted and the `"` character is typed, the work will be surrounded by `"`s.
47
55
  */
48
56
  export declare class Editor extends Base {
49
57
  #private;
@@ -2,6 +2,23 @@ import { Base } from "../base/index.js";
2
2
  /**
3
3
  * Enhances the `textarea` element with controls to add content and keyboard shortcuts. Compared to other WYSIWYG editors, the `text` value is just a `string`, so you can easily store it in a database or manipulate it without learning a separate API.
4
4
  *
5
+ * - Automatically adds closing characters for `keyPairs`. For example, when
6
+ * typing `(`, `)` will be inserted and typed over when reached. All content
7
+ * with `data-type="wrap"` is also added to `keyPairs`.
8
+ * - Highlights the first word of the text inserted if it contains letters.
9
+ * - Automatically increments/decrements ordered lists.
10
+ * - Adds the starting character to the next line for `block` content.
11
+ * - On double click, highlight is corrected to only highlight the current word
12
+ * without space around it.
13
+ * - `tab` key will indent or dedent (+shift) instead of focus change if the
14
+ * selection is within a code block (three backticks).
15
+ * - When text is highlighted and a `wrap` character `keyPair` is typed, the
16
+ * highlighted text will be wrapped with the character instead of removing it.
17
+ * For example, if a word is highlighted and the `"` character is typed, the
18
+ * work will be surrounded by `"`s.
19
+ *
20
+ * ### Trigger attributes
21
+ *
5
22
  * `data-value`
6
23
  *
7
24
  * Set the value of the text to be inserted using the `data-value` attribute on the `trigger`.
@@ -18,15 +35,6 @@ import { Base } from "../base/index.js";
18
35
  *
19
36
  * Add a `ctrl`/`meta` keyboard shortcut for the content based on the `data-key` attribute.
20
37
  *
21
- * Other features:
22
- *
23
- * - Automatically adds closing characters for `keyPairs`. For example, when typing `(`, `)` will be inserted and typed over when reached. All content with `data-type="wrap"` is also added to `keyPairs`.
24
- * - Highlights the first word of the text inserted if it contains letters.
25
- * - Automatically increments/decrements ordered lists.
26
- * - Adds the starting character to the next line for `block` content.
27
- * - On double click, highlight is corrected to only highlight the current word without space around it.
28
- * - `tab` key will indent or dedent (+shift) instead of focus change if the selection is within a code block (three backticks).
29
- * - When text is highlighted and a `wrap` character `keyPair` is typed, the highlighted text will be wrapped with the character instead of removing it. For example, if a word is highlighted and the `"` character is typed, the work will be surrounded by `"`s.
30
38
  */
31
39
  export class Editor extends Base {
32
40
  /** Array of `keyPair` characters that have been opened. */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./announcer/index.js";
2
- export * from "./base/index.js";
2
+ export { Base, type BaseAttributes } from "./base/index.js";
3
3
  export * from "./contextmenu/index.js";
4
4
  export * from "./copy/index.js";
5
5
  export * from "./dialog/index.js";
@@ -9,5 +9,6 @@ export * from "./intersect/index.js";
9
9
  export * from "./prefetch/index.js";
10
10
  export * from "./share/index.js";
11
11
  export * from "./tablesort/index.js";
12
+ export * from "./tabs/index.js";
12
13
  export * from "./wakelock/index.js";
13
14
  export * from "./youtube/index.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./announcer/index.js";
2
- export * from "./base/index.js";
2
+ export { Base } from "./base/index.js";
3
3
  export * from "./contextmenu/index.js";
4
4
  export * from "./copy/index.js";
5
5
  export * from "./dialog/index.js";
@@ -9,5 +9,6 @@ export * from "./intersect/index.js";
9
9
  export * from "./prefetch/index.js";
10
10
  export * from "./share/index.js";
11
11
  export * from "./tablesort/index.js";
12
+ export * from "./tabs/index.js";
12
13
  export * from "./wakelock/index.js";
13
14
  export * from "./youtube/index.js";
@@ -8,6 +8,8 @@ type IntersectCallback = () => any;
8
8
  *
9
9
  * Use `onIntersect` and `onExit` to customize further with JavaScript.
10
10
  *
11
+ * ### Attributes
12
+ *
11
13
  * `threshold`
12
14
  *
13
15
  * Specify a `threshold` between `0` and `1` to determine how much of the `trigger` should be visible for the intersection to occur.
@@ -4,6 +4,8 @@ import { Base } from "../base/index.js";
4
4
  *
5
5
  * Use `onIntersect` and `onExit` to customize further with JavaScript.
6
6
  *
7
+ * ### Attributes
8
+ *
7
9
  * `threshold`
8
10
  *
9
11
  * Specify a `threshold` between `0` and `1` to determine how much of the `trigger` should be visible for the intersection to occur.
@@ -10,6 +10,8 @@ export type PrefetchAttributes = BaseAttributes & {
10
10
  *
11
11
  * 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.
12
12
  *
13
+ * ### Attributes
14
+ *
13
15
  * `strategy`
14
16
  *
15
17
  * Set the `strategy` attribute to specify the when the prefetch will take place.
@@ -4,6 +4,8 @@ import { Base } from "../base/index.js";
4
4
  *
5
5
  * 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.
6
6
  *
7
+ * ### Attributes
8
+ *
7
9
  * `strategy`
8
10
  *
9
11
  * Set the `strategy` attribute to specify the when the prefetch will take place.
@@ -88,12 +90,12 @@ export class Prefetch extends Base {
88
90
  * @param options Prefetch options.
89
91
  */
90
92
  prefetch(options = {
91
- anchors: this.getTrigger(),
93
+ anchors: this.getTrigger(HTMLAnchorElement),
92
94
  prerender: this.#prerender,
93
95
  strategy: this.#strategy,
94
96
  }) {
95
97
  // defaults if partially defined
96
- const { anchors = this.getTrigger(), prerender = this.#prerender, strategy = this.#strategy, } = options;
98
+ const { anchors = this.getTrigger(HTMLAnchorElement), prerender = this.#prerender, strategy = this.#strategy, } = options;
97
99
  let prefetchTimer;
98
100
  /**
99
101
  * @param delay ms delay - for `hover`
@@ -1,8 +1,15 @@
1
1
  import { Copy, type CopyAttributes } from "../copy/index.js";
2
2
  export type ShareAttributes = CopyAttributes;
3
3
  /**
4
- * Uses the [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
4
+ * Uses the
5
+ * [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
5
6
  * to share a url. If `share` is not supported, falls back to copy the text instead.
7
+ *
8
+ * ### Attributes
9
+ *
10
+ * `value`
11
+ *
12
+ * Text to share.
6
13
  */
7
14
  export declare class Share extends Copy {
8
15
  constructor();
@@ -1,7 +1,14 @@
1
1
  import { Copy } from "../copy/index.js";
2
2
  /**
3
- * Uses the [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
3
+ * Uses the
4
+ * [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
4
5
  * to share a url. If `share` is not supported, falls back to copy the text instead.
6
+ *
7
+ * ### Attributes
8
+ *
9
+ * `value`
10
+ *
11
+ * Text to share.
5
12
  */
6
13
  export class Share extends Copy {
7
14
  constructor() {
@@ -16,6 +16,9 @@ export class TableSort extends Base {
16
16
  constructor() {
17
17
  super();
18
18
  }
19
+ get #th() {
20
+ return this.getTrigger(HTMLTableCellElement);
21
+ }
19
22
  /**
20
23
  * Removes `data-asc` or `data-desc` from other triggers then sets the correct attribute on the selected trigger.
21
24
  *
@@ -25,7 +28,7 @@ export class TableSort extends Base {
25
28
  #setAttributes(trigger) {
26
29
  const asc = "data-asc";
27
30
  const desc = "data-desc";
28
- for (const t of this.getTrigger()) {
31
+ for (const t of this.getTrigger(HTMLTableCellElement)) {
29
32
  if (t !== trigger) {
30
33
  t.removeAttribute(asc);
31
34
  t.removeAttribute(desc);
@@ -42,7 +45,7 @@ export class TableSort extends Base {
42
45
  }
43
46
  mount() {
44
47
  const tbody = this.getContent(HTMLTableSectionElement);
45
- for (const trigger of this.getTrigger()) {
48
+ for (const trigger of this.#th) {
46
49
  trigger.tabIndex = 0;
47
50
  trigger.role = "button";
48
51
  const listener = () => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { define } from "../util/define.js";
2
+ import { Tabs } from "./index.js";
3
+ define("drab-tabs", Tabs);
@@ -0,0 +1,37 @@
1
+ import { Base, type BaseAttributes } from "../base/index.js";
2
+ export type TabAttributes = BaseAttributes & {
3
+ orientation?: "horizontal" | "vertical";
4
+ };
5
+ /**
6
+ * Progressively enhance a list of links and content to be tabs by
7
+ * wrapping with the `Tabs` element. Each `trigger` should be an
8
+ * `HTMLAnchorElement` with the `href` attribute set to the `id` of the
9
+ * corresponding tab panel.
10
+ *
11
+ * > Tip: Set the `height` of the element the `panel`s are contained in with
12
+ * > CSS to prevent layout shift when JS is loaded.
13
+ *
14
+ * This element is based on
15
+ * [Chris Ferdinandi's Toggle Tabs](https://gomakethings.com/a-web-component-ui-library-for-people-who-love-html/#toggle-tabs)
16
+ * design.
17
+ *
18
+ * [ARIA Reference](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
19
+ *
20
+ * - Sets the correct ARIA attributes on each element.
21
+ * - Supports keyboard navigation based on the `orientation` attribute.
22
+ * - First tab is selected by default if no `aria-selected="true"` attribute is
23
+ * found on another tab.
24
+ * - `tablist` is calculated based on the deepest common parent of the tabs,
25
+ * throws an error if not found.
26
+ *
27
+ * ### Attributes
28
+ *
29
+ * `orientation`
30
+ *
31
+ * Set `orientation="vertical"` if the `tablist` element is displayed vertically.
32
+ */
33
+ export declare class Tabs extends Base {
34
+ #private;
35
+ constructor();
36
+ mount(): void;
37
+ }
@@ -0,0 +1,147 @@
1
+ import { Base } from "../base/index.js";
2
+ /**
3
+ * Progressively enhance a list of links and content to be tabs by
4
+ * wrapping with the `Tabs` element. Each `trigger` should be an
5
+ * `HTMLAnchorElement` with the `href` attribute set to the `id` of the
6
+ * corresponding tab panel.
7
+ *
8
+ * > Tip: Set the `height` of the element the `panel`s are contained in with
9
+ * > CSS to prevent layout shift when JS is loaded.
10
+ *
11
+ * This element is based on
12
+ * [Chris Ferdinandi's Toggle Tabs](https://gomakethings.com/a-web-component-ui-library-for-people-who-love-html/#toggle-tabs)
13
+ * design.
14
+ *
15
+ * [ARIA Reference](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
16
+ *
17
+ * - Sets the correct ARIA attributes on each element.
18
+ * - Supports keyboard navigation based on the `orientation` attribute.
19
+ * - First tab is selected by default if no `aria-selected="true"` attribute is
20
+ * found on another tab.
21
+ * - `tablist` is calculated based on the deepest common parent of the tabs,
22
+ * throws an error if not found.
23
+ *
24
+ * ### Attributes
25
+ *
26
+ * `orientation`
27
+ *
28
+ * Set `orientation="vertical"` if the `tablist` element is displayed vertically.
29
+ */
30
+ export class Tabs extends Base {
31
+ /** Supported keys for keyboard navigation. */
32
+ #keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
33
+ /** Currently selected tab. */
34
+ #selected = { index: 0 };
35
+ constructor() {
36
+ super();
37
+ }
38
+ /** User provided orientation of the `tablist`. */
39
+ get #orientation() {
40
+ return this.getAttribute("orientation") ?? "horizontal";
41
+ }
42
+ get #tabs() {
43
+ return this.getTrigger(HTMLAnchorElement);
44
+ }
45
+ /**
46
+ * @param tab
47
+ * @returns The ancestors of the tab up to `this`.
48
+ */
49
+ #ancestors(tab) {
50
+ const ancestors = new Set();
51
+ let current = tab;
52
+ while ((current = current?.parentElement) && current !== this) {
53
+ ancestors.add(current);
54
+ }
55
+ return ancestors;
56
+ }
57
+ /** A map of each `tab` and its corresponding `panel`. */
58
+ get #map() {
59
+ const map = new Map();
60
+ for (const tab of this.#tabs) {
61
+ const panel = this.querySelector(tab.hash);
62
+ if (!(panel instanceof HTMLElement))
63
+ throw new Error(`Tabs: No HTMLElement with ID of ${tab.hash} found.`);
64
+ map.set(tab, panel);
65
+ }
66
+ return map;
67
+ }
68
+ mount() {
69
+ // create tablist
70
+ const [first, ...rest] = this.#tabs;
71
+ let common = this.#ancestors(first);
72
+ for (let i = 0; i < rest.length; i++) {
73
+ common = common.intersection(this.#ancestors(rest[i]));
74
+ }
75
+ const [tablist] = common;
76
+ if (!tablist)
77
+ throw new Error("Tabs: No common parent element found for triggers.");
78
+ tablist.role = "tablist";
79
+ tablist.ariaOrientation = this.#orientation;
80
+ // enhance tabs/panels
81
+ let index = 0;
82
+ for (const [tab, panel] of this.#map) {
83
+ tab.role = "tab";
84
+ tab.id = `tab-${panel.id}`;
85
+ tab.setAttribute("aria-controls", panel.id);
86
+ if (tab.ariaSelected)
87
+ this.#selected = { tab, index };
88
+ panel.role = "tabpanel";
89
+ panel.setAttribute("aria-labelledby", tab.id);
90
+ tab.addEventListener(this.event, (e) => {
91
+ e.preventDefault();
92
+ for (const [t, p] of this.#map) {
93
+ if (t === tab) {
94
+ // show current
95
+ t.ariaSelected = "true";
96
+ t.tabIndex = 0;
97
+ p.hidden = false;
98
+ }
99
+ else {
100
+ // hide others
101
+ t.ariaSelected = "false";
102
+ t.tabIndex = -1;
103
+ p.hidden = true;
104
+ }
105
+ }
106
+ });
107
+ index++;
108
+ }
109
+ // fallback to first
110
+ if (!this.#selected.tab)
111
+ this.#selected.tab = this.#tabs[0];
112
+ // select the current tab
113
+ this.#selected.tab?.click();
114
+ // handle keyboard navigation
115
+ this.addEventListener("keydown", (e) => {
116
+ const i = this.#keys.indexOf(e.key);
117
+ if (i === -1)
118
+ return;
119
+ const previousIndex = this.#selected.index;
120
+ const vertical = this.#orientation === "vertical";
121
+ if (((!vertical && i === 0) || (vertical && i === 1)) &&
122
+ this.#tabs[this.#selected.index + 1]) {
123
+ // next
124
+ this.#selected.tab = this.#tabs[++this.#selected.index];
125
+ }
126
+ else if (((!vertical && i === 2) || (vertical && i === 3)) &&
127
+ this.#tabs[this.#selected.index - 1]) {
128
+ // previous
129
+ this.#selected.tab = this.#tabs[--this.#selected.index];
130
+ }
131
+ else if (i === 4) {
132
+ // home
133
+ this.#selected = { tab: this.#tabs[0], index: 0 };
134
+ }
135
+ else if (i === 5) {
136
+ // end
137
+ const index = this.#tabs.length - 1;
138
+ this.#selected = { tab: this.#tabs[index], index };
139
+ }
140
+ if (this.#selected.index === previousIndex)
141
+ return;
142
+ e.preventDefault();
143
+ this.#selected.tab?.click();
144
+ this.#selected.tab?.focus();
145
+ });
146
+ }
147
+ }
@@ -0,0 +1,7 @@
1
+ import type { Constructor } from "../base/index.js";
2
+ /**
3
+ * @param actual Element to validate.
4
+ * @param expected Constructor of the expected element.
5
+ * @returns If valid returns `actual` otherwise throws `TypeError`.
6
+ */
7
+ export declare const validate: <T extends HTMLElement>(actual: unknown, expected: Constructor<T>) => T;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @param actual Element to validate.
3
+ * @param expected Constructor of the expected element.
4
+ * @returns If valid returns `actual` otherwise throws `TypeError`.
5
+ */
6
+ export const validate = (actual, expected) => {
7
+ if (!(actual instanceof expected))
8
+ throw new TypeError(`${actual} is not an instance of ${expected.name}.`);
9
+ return actual;
10
+ };