drab 6.2.1 → 6.3.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.
@@ -10,6 +10,8 @@
10
10
  * [avoid duplicate regions](https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/#limit-the-number-of-live-regions-on-the-page)
11
11
  * (see below).
12
12
  *
13
+ * ### Attributes
14
+ *
13
15
  * `aria-live`
14
16
  *
15
17
  * By default, the announcer is created with the
@@ -11,6 +11,8 @@ import { define } from "../util/define.js";
11
11
  * [avoid duplicate regions](https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/#limit-the-number-of-live-regions-on-the-page)
12
12
  * (see below).
13
13
  *
14
+ * ### Attributes
15
+ *
14
16
  * `aria-live`
15
17
  *
16
18
  * By default, the announcer is created with the
@@ -3,6 +3,7 @@ export type BaseAttributes = {
3
3
  content?: string;
4
4
  swap?: string;
5
5
  };
6
+ type Constructor<T> = new (...args: any[]) => T;
6
7
  /**
7
8
  * Each element in the library extends the `Base` class. It provides methods
8
9
  * for selecting elements via HTML attributes along with other helpers.
@@ -32,19 +33,21 @@ export declare class Base extends HTMLElement {
32
33
  */
33
34
  announce(message: string): void;
34
35
  /**
36
+ * @param instance The instance of the desired element to validate against,
37
+ * ex: `HTMLButtonElement`. Defaults to `HTMLElement`.
35
38
  * @returns All of the elements that match the `trigger` selector.
36
39
  * @default this.querySelectorAll("[data-trigger]")
37
40
  */
38
- getTrigger<T extends HTMLElement = HTMLElement>(): NodeListOf<T>;
41
+ getTrigger<T extends HTMLElement>(instance: Constructor<T>): NodeListOf<T>;
42
+ getTrigger(): NodeListOf<HTMLElement>;
39
43
  /**
40
44
  * @param instance The instance of the desired element to validate against,
41
45
  * ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
42
46
  * @returns The element that matches the `content` selector.
43
47
  * @default this.querySelector("[data-content]")
44
48
  */
45
- getContent<T extends HTMLElement = HTMLElement>(instance?: {
46
- new (): T;
47
- }): T;
49
+ getContent<T extends HTMLElement>(instance: Constructor<T>): T;
50
+ getContent(): HTMLElement;
48
51
  /**
49
52
  * Finds the `HTMLElement | HTMLTemplateElement` via the `swap` selector and
50
53
  * swaps `this.content()` with the content of the element found.
@@ -84,3 +87,4 @@ export declare class Base extends HTMLElement {
84
87
  /** Called when custom element is removed from the page. */
85
88
  disconnectedCallback(): void;
86
89
  }
90
+ export {};
@@ -30,7 +30,7 @@ export class Base extends HTMLElement {
30
30
  * @default "click"
31
31
  */
32
32
  get event() {
33
- return (this.getAttribute("event") ?? "click");
33
+ return this.getAttribute("event") ?? "click";
34
34
  }
35
35
  set event(value) {
36
36
  this.setAttribute("event", value);
@@ -41,25 +41,14 @@ export class Base extends HTMLElement {
41
41
  announce(message) {
42
42
  Base.#announcer.announce(message);
43
43
  }
44
- /**
45
- * @returns All of the elements that match the `trigger` selector.
46
- * @default this.querySelectorAll("[data-trigger]")
47
- */
48
- getTrigger() {
44
+ getTrigger(instance = HTMLElement) {
49
45
  const triggers = this.querySelectorAll(this.getAttribute("trigger") ?? "[data-trigger]");
46
+ for (const trigger of triggers)
47
+ this.#validate(trigger, instance);
50
48
  return triggers;
51
49
  }
52
- /**
53
- * @param instance The instance of the desired element to validate against,
54
- * ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
55
- * @returns The element that matches the `content` selector.
56
- * @default this.querySelector("[data-content]")
57
- */
58
50
  getContent(instance = HTMLElement) {
59
- const content = this.querySelector(this.getAttribute("content") ?? "[data-content]");
60
- if (content instanceof instance)
61
- return content;
62
- throw new Error("Content not found");
51
+ return this.#validate(this.querySelector(this.getAttribute("content") ?? "[data-content]"), instance);
63
52
  }
64
53
  /**
65
54
  * Finds the `HTMLElement | HTMLTemplateElement` via the `swap` selector and
@@ -130,4 +119,14 @@ export class Base extends HTMLElement {
130
119
  this.destroy();
131
120
  this.#listenerController.abort();
132
121
  }
122
+ /**
123
+ * @param actual Element to validate.
124
+ * @param expected Constructor of the expected element.
125
+ * @returns If valid returns `actual` otherwise throws `TypeError`.
126
+ */
127
+ #validate(actual, expected) {
128
+ if (!(actual instanceof expected))
129
+ throw new TypeError(`${actual} is not an instance of ${expected.name}.`);
130
+ return actual;
131
+ }
133
132
  }
@@ -6,7 +6,7 @@ export type ContextMenuAttributes = BaseAttributes;
6
6
  export declare class ContextMenu extends Base {
7
7
  #private;
8
8
  constructor();
9
- show(e: MouseEvent | TouchEvent): Promise<void>;
10
- hide(): Promise<void>;
9
+ show(e: MouseEvent | TouchEvent): void;
10
+ hide(): void;
11
11
  mount(): void;
12
12
  }
@@ -13,7 +13,7 @@ export class ContextMenu extends Base {
13
13
  this.getContent().style.left = `${value.x}px`;
14
14
  this.getContent().style.top = `${value.y}px`;
15
15
  }
16
- async show(e) {
16
+ show(e) {
17
17
  // find coordinates of the click
18
18
  const scrollY = window.scrollY;
19
19
  const scrollX = window.scrollX;
@@ -36,7 +36,7 @@ export class ContextMenu extends Base {
36
36
  }
37
37
  this.#coordinates = { x, y };
38
38
  }
39
- async hide() {
39
+ hide() {
40
40
  this.getContent().removeAttribute("data-open");
41
41
  }
42
42
  mount() {
@@ -57,9 +57,8 @@ export class ContextMenu extends Base {
57
57
  this.triggerListener(resetTimer, "touchcancel", { passive: true });
58
58
  // keyboard
59
59
  this.safeListener("keydown", (e) => {
60
- if (e.key === "Escape") {
60
+ if (e.key === "Escape")
61
61
  this.hide();
62
- }
63
62
  });
64
63
  }
65
64
  }
@@ -1,10 +1,30 @@
1
- import { BaseCopy, type BaseCopyAttributes } from "../base/copy/index.js";
2
- export type CopyAttributes = BaseCopyAttributes;
1
+ import { Base, type BaseAttributes } from "../base/index.js";
2
+ export type CopyAttributes = BaseAttributes & {
3
+ value: string;
4
+ };
3
5
  /**
4
- * 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)
5
8
  * to copy text.
9
+ *
10
+ * ### Attributes
11
+ *
12
+ * `value`
13
+ *
14
+ * Text to copy.
6
15
  */
7
- export declare class Copy extends BaseCopy {
16
+ export declare class Copy extends Base {
8
17
  constructor();
18
+ /**
19
+ * The value to copy.
20
+ *
21
+ * @default ""
22
+ */
23
+ get value(): string;
24
+ set value(value: string);
25
+ /**
26
+ * @param value The `value` to copy
27
+ */
28
+ copy(value?: string): Promise<void>;
9
29
  mount(): void;
10
30
  }
@@ -1,13 +1,39 @@
1
- import { BaseCopy } from "../base/copy/index.js";
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
- export class Copy extends BaseCopy {
13
+ export class Copy extends Base {
7
14
  constructor() {
8
15
  super();
9
16
  }
17
+ /**
18
+ * The value to copy.
19
+ *
20
+ * @default ""
21
+ */
22
+ get value() {
23
+ return this.getAttribute("value") ?? "";
24
+ }
25
+ set value(value) {
26
+ this.setAttribute("value", value);
27
+ }
28
+ /**
29
+ * @param value The `value` to copy
30
+ */
31
+ copy(value = this.value) {
32
+ this.announce(`copied ${value} to clipboard`);
33
+ this.swapContent();
34
+ return navigator.clipboard.writeText(value);
35
+ }
10
36
  mount() {
11
- this.triggerListener(async () => await this.copy());
37
+ this.triggerListener(() => this.copy());
12
38
  }
13
39
  }
@@ -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
@@ -22,10 +24,10 @@ export declare class Dialog extends Base {
22
24
  /** The `HTMLDialogElement` within the element. */
23
25
  get dialog(): HTMLDialogElement;
24
26
  /** `HTMLDialogElement.showModal()` with animation. */
25
- show(): Promise<void>;
27
+ show(): void;
26
28
  /** `HTMLDialogElement.close()` with animation. */
27
- close(): Promise<void>;
29
+ close(): void;
28
30
  /** `show` or `close` depending on the dialog's `open` attribute. */
29
- toggle(): Promise<void>;
31
+ toggle(): void;
30
32
  mount(): void;
31
33
  }
@@ -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
@@ -35,17 +37,17 @@ export class Dialog extends Base {
35
37
  }
36
38
  }
37
39
  /** `HTMLDialogElement.showModal()` with animation. */
38
- async show() {
40
+ show() {
39
41
  this.dialog.showModal();
40
42
  this.#toggleBodyScroll(true);
41
43
  }
42
44
  /** `HTMLDialogElement.close()` with animation. */
43
- async close() {
45
+ close() {
44
46
  this.#toggleBodyScroll(false);
45
47
  this.dialog.close();
46
48
  }
47
49
  /** `show` or `close` depending on the dialog's `open` attribute. */
48
- async toggle() {
50
+ toggle() {
49
51
  if (this.dialog.open)
50
52
  this.close();
51
53
  else
@@ -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
@@ -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
@@ -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,15 +1,21 @@
1
- import { BaseCopy, type BaseCopyAttributes } from "../base/copy/index.js";
2
- export type ShareAttributes = BaseCopyAttributes;
1
+ import { Copy, type CopyAttributes } from "../copy/index.js";
2
+ export type ShareAttributes = CopyAttributes;
3
3
  /**
4
- * Uses the [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share) to share a url. If `share` is not supported, falls back to copy the text instead.
4
+ * Uses the
5
+ * [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
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.
5
13
  */
6
- export declare class Share extends BaseCopy {
14
+ export declare class Share extends Copy {
7
15
  constructor();
8
16
  /**
9
17
  * Shares or copies the `value`.
10
18
  * @param url The `url` to share, defaults to `this.value`
11
- * @returns An object containing a `result` - whether the `url` was copied or shared
12
- * depending on browser support.
13
19
  */
14
20
  share(url?: string): Promise<void>;
15
21
  mount(): void;
@@ -1,34 +1,33 @@
1
- import { BaseCopy } from "../base/copy/index.js";
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) to share a url. If `share` is not supported, falls back to copy the text instead.
3
+ * Uses the
4
+ * [Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)
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.
4
12
  */
5
- export class Share extends BaseCopy {
13
+ export class Share extends Copy {
6
14
  constructor() {
7
15
  super();
8
16
  }
9
17
  /**
10
18
  * Shares or copies the `value`.
11
19
  * @param url The `url` to share, defaults to `this.value`
12
- * @returns An object containing a `result` - whether the `url` was copied or shared
13
- * depending on browser support.
14
20
  */
15
- async share(url = this.value) {
21
+ share(url = this.value) {
16
22
  if (navigator.canShare && navigator.canShare({ url })) {
17
- try {
18
- await navigator.share({ url });
19
- }
20
- catch (error) {
21
- if (error?.name !== "AbortError") {
22
- console.error(error);
23
- }
24
- }
23
+ return navigator.share({ url });
25
24
  }
26
25
  else {
27
26
  // progressively enhance, copy the link
28
- this.copy();
27
+ return this.copy();
29
28
  }
30
29
  }
31
30
  mount() {
32
- this.triggerListener(async () => await this.share());
31
+ this.triggerListener(() => this.share());
33
32
  }
34
33
  }
@@ -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 panels 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 panels 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
+ }
@@ -4,19 +4,29 @@ export type WakeLockAttributes = BaseAttributes & {
4
4
  locked?: boolean;
5
5
  };
6
6
  /**
7
- * `WakeLock` uses the [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) to ensure the screen does not turn off when viewing the page on supported devices. Use your best judgement for when this is necessary, for example, if you have a timer that needs to stay on, or you are displaying a QR code.
7
+ * `WakeLock` uses the
8
+ * [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API)
9
+ * to ensure the screen does not turn off when viewing the page on supported devices.
10
+ * Use your best judgement for when this is necessary, for example, if you have a timer
11
+ * that needs to stay on, or you are displaying a QR code.
8
12
  *
9
- * - WakeLock can be toggled with a `trigger`, or will be requested if the element has a `locked` attribute when connected.
10
13
  * - Use `content` and `swap` elements to adjust the UI based on the current state.
11
14
  * - `request` and `release` methods are provided to set the WakeLock with JavaScript.
12
15
  * - `trigger` is disabled if not supported.
13
16
  * - WakeLock is released when the element is removed from the DOM.
14
17
  *
18
+ * ### Attributes
19
+ *
15
20
  * `auto-lock`
16
21
  *
17
- * - By default, the WakeLock will be released when the tab is not active. Use the `auto-lock` attribute to automatically request the WakeLock when the user views the tab again.
22
+ * By default, the WakeLock will be released when the tab is not active. Use the
23
+ * `auto-lock` attribute to automatically request the WakeLock when the user views
24
+ * the tab again.
18
25
  *
26
+ * `locked`
19
27
  *
28
+ * WakeLock can be toggled with a `trigger`, or will be requested if the element has
29
+ * a `locked` attribute when connected.
20
30
  */
21
31
  export declare class WakeLock extends Base {
22
32
  #private;
@@ -1,18 +1,28 @@
1
1
  import { Base } from "../base/index.js";
2
2
  /**
3
- * `WakeLock` uses the [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) to ensure the screen does not turn off when viewing the page on supported devices. Use your best judgement for when this is necessary, for example, if you have a timer that needs to stay on, or you are displaying a QR code.
3
+ * `WakeLock` uses the
4
+ * [WakeLock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API)
5
+ * to ensure the screen does not turn off when viewing the page on supported devices.
6
+ * Use your best judgement for when this is necessary, for example, if you have a timer
7
+ * that needs to stay on, or you are displaying a QR code.
4
8
  *
5
- * - WakeLock can be toggled with a `trigger`, or will be requested if the element has a `locked` attribute when connected.
6
9
  * - Use `content` and `swap` elements to adjust the UI based on the current state.
7
10
  * - `request` and `release` methods are provided to set the WakeLock with JavaScript.
8
11
  * - `trigger` is disabled if not supported.
9
12
  * - WakeLock is released when the element is removed from the DOM.
10
13
  *
14
+ * ### Attributes
15
+ *
11
16
  * `auto-lock`
12
17
  *
13
- * - By default, the WakeLock will be released when the tab is not active. Use the `auto-lock` attribute to automatically request the WakeLock when the user views the tab again.
18
+ * By default, the WakeLock will be released when the tab is not active. Use the
19
+ * `auto-lock` attribute to automatically request the WakeLock when the user views
20
+ * the tab again.
14
21
  *
22
+ * `locked`
15
23
  *
24
+ * WakeLock can be toggled with a `trigger`, or will be requested if the element has
25
+ * a `locked` attribute when connected.
16
26
  */
17
27
  export class WakeLock extends Base {
18
28
  wakeLock = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "drab",
3
3
  "description": "Interactivity for You",
4
- "version": "6.2.1",
4
+ "version": "6.3.0",
5
5
  "homepage": "https://drab.robino.dev",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -1,19 +0,0 @@
1
- import { Base, type BaseAttributes } from "../index.js";
2
- export type BaseCopyAttributes = BaseAttributes & {
3
- value: string;
4
- };
5
- export declare class BaseCopy extends Base {
6
- constructor();
7
- /**
8
- * The value to copy or share.
9
- *
10
- * @default "" the empty string
11
- */
12
- get value(): string;
13
- set value(value: string);
14
- /**
15
- * Copies the `text`.
16
- * @param text The `text` to share
17
- */
18
- copy(text?: string): Promise<void>;
19
- }
@@ -1,26 +0,0 @@
1
- import { Base } from "../index.js";
2
- export class BaseCopy extends Base {
3
- constructor() {
4
- super();
5
- }
6
- /**
7
- * The value to copy or share.
8
- *
9
- * @default "" the empty string
10
- */
11
- get value() {
12
- return this.getAttribute("value") ?? "";
13
- }
14
- set value(value) {
15
- this.setAttribute("value", value);
16
- }
17
- /**
18
- * Copies the `text`.
19
- * @param text The `text` to share
20
- */
21
- async copy(text = this.value) {
22
- this.announce(`copied ${text} to clipboard`);
23
- await navigator.clipboard.writeText(text);
24
- this.swapContent();
25
- }
26
- }