drab 6.0.0 → 6.1.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { define } from "../util/define.js";
2
+ import { Announcer } from "./index.js";
3
+ define("drab-announcer", Announcer);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Use the `Announcer` element to create a visually hidden ARIA live region
3
+ * that announces content changes to screen readers. Use this element when you
4
+ * need to announce changes to screen readers that something has changed. If changed
5
+ * element is visible on the page, add the appropriate ARIA live attribute to the
6
+ * visible element instead of using this announcer.
7
+ *
8
+ * It's recommended to create this element with JavaScript using the `Announcer.init` method,
9
+ * then you can reuse the same announcer throughout the application to
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
+ * (see below).
12
+ *
13
+ * `aria-live`
14
+ *
15
+ * By default, the announcer is created with the
16
+ * [`polite` ARIA live attribute](https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-1/#1.-using-the-aria-live-attribute).
17
+ *
18
+ * @example
19
+ *
20
+ * ```ts
21
+ * import { Announcer } from "drab/announcer";
22
+ *
23
+ * // creates and appends a new announcer to the body element
24
+ * const announcer = Announcer.init();
25
+ *
26
+ * // create announcement
27
+ * announcer.announce("message");
28
+ * ```
29
+ *
30
+ * > The `Base` element creates a single `Announcer` to share between all elements
31
+ * > that can be accessed through `this.announce`. If you are using one of drab's other
32
+ * > elements you can call `announce` directly on the element to announce changes.
33
+ */
34
+ export declare class Announcer extends HTMLElement {
35
+ constructor();
36
+ connectedCallback(): void;
37
+ /**
38
+ * @param message message to announce to screen readers
39
+ */
40
+ announce(message: string): void;
41
+ /**
42
+ * Helper method to create a new `Announcer` element named `drab-announcer`
43
+ * and append the element to the `<body>` tag. If an announcer already exists
44
+ * on the page it will return the existing element.
45
+ *
46
+ * @returns the created or existing `Announcer` element
47
+ */
48
+ static init(): Announcer;
49
+ }
@@ -0,0 +1,80 @@
1
+ import { define } from "../util/define.js";
2
+ /**
3
+ * Use the `Announcer` element to create a visually hidden ARIA live region
4
+ * that announces content changes to screen readers. Use this element when you
5
+ * need to announce changes to screen readers that something has changed. If changed
6
+ * element is visible on the page, add the appropriate ARIA live attribute to the
7
+ * visible element instead of using this announcer.
8
+ *
9
+ * It's recommended to create this element with JavaScript using the `Announcer.init` method,
10
+ * then you can reuse the same announcer throughout the application to
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
+ * (see below).
13
+ *
14
+ * `aria-live`
15
+ *
16
+ * By default, the announcer is created with the
17
+ * [`polite` ARIA live attribute](https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-1/#1.-using-the-aria-live-attribute).
18
+ *
19
+ * @example
20
+ *
21
+ * ```ts
22
+ * import { Announcer } from "drab/announcer";
23
+ *
24
+ * // creates and appends a new announcer to the body element
25
+ * const announcer = Announcer.init();
26
+ *
27
+ * // create announcement
28
+ * announcer.announce("message");
29
+ * ```
30
+ *
31
+ * > The `Base` element creates a single `Announcer` to share between all elements
32
+ * > that can be accessed through `this.announce`. If you are using one of drab's other
33
+ * > elements you can call `announce` directly on the element to announce changes.
34
+ */
35
+ export class Announcer extends HTMLElement {
36
+ constructor() {
37
+ super();
38
+ }
39
+ connectedCallback() {
40
+ this.style.position = "absolute";
41
+ this.style.width = "1px";
42
+ this.style.height = "1px";
43
+ this.style.padding = "0";
44
+ this.style.margin = "-1px";
45
+ this.style.overflow = "hidden";
46
+ this.style.clipPath = "rect(0, 0, 0, 0)";
47
+ this.style.whiteSpace = "nowrap";
48
+ this.style.borderWidth = "0";
49
+ if (!this.ariaLive)
50
+ this.ariaLive = "polite";
51
+ }
52
+ /**
53
+ * @param message message to announce to screen readers
54
+ */
55
+ announce(message) {
56
+ // this ensures multiple messages will be read in succession
57
+ const span = document.createElement("span");
58
+ span.textContent = message;
59
+ this.append(span);
60
+ // https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-2/#empty-the-live-region-and-wait-a-bit-in-between-updates
61
+ setTimeout(() => span.remove(), 10000);
62
+ }
63
+ /**
64
+ * Helper method to create a new `Announcer` element named `drab-announcer`
65
+ * and append the element to the `<body>` tag. If an announcer already exists
66
+ * on the page it will return the existing element.
67
+ *
68
+ * @returns the created or existing `Announcer` element
69
+ */
70
+ static init() {
71
+ define("drab-announcer", this);
72
+ const name = "drab-announcer";
73
+ let announcer = document.querySelector(name);
74
+ if (!announcer) {
75
+ announcer = document.createElement(name);
76
+ document.body.append(announcer);
77
+ }
78
+ return announcer;
79
+ }
80
+ }
@@ -19,6 +19,7 @@ export class BaseCopy extends Base {
19
19
  * @param text The `text` to share
20
20
  */
21
21
  async copy(text = this.value) {
22
+ this.announce(`copied ${text} to clipboard`);
22
23
  await navigator.clipboard.writeText(text);
23
24
  this.swapContent();
24
25
  }
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Base } from "./index.js";
2
- customElements.define("drab-base", Base);
3
+ define("drab-base", Base);
@@ -27,14 +27,18 @@ export declare class Base extends HTMLElement {
27
27
  */
28
28
  get event(): keyof HTMLElementEventMap;
29
29
  set event(value: keyof HTMLElementEventMap);
30
+ /**
31
+ * @param message message to announce to screen readers
32
+ */
33
+ announce(message: string): void;
30
34
  /**
31
35
  * @returns All of the elements that match the `trigger` selector.
32
36
  * @default this.querySelectorAll("[data-trigger]")
33
37
  */
34
38
  getTrigger<T extends HTMLElement = HTMLElement>(): NodeListOf<T>;
35
39
  /**
36
- * @param instance The instance of the desired element, ex: `HTMLDialogElement`.
37
- * Defaults to `HTMLElement`.
40
+ * @param instance The instance of the desired element to validate against,
41
+ * ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
38
42
  * @returns The element that matches the `content` selector.
39
43
  * @default this.querySelector("[data-content]")
40
44
  */
@@ -67,16 +71,12 @@ export declare class Base extends HTMLElement {
67
71
  * 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.
68
72
  */
69
73
  mount(): void;
70
- /**
71
- * Called when custom element is added to the page.
72
- */
74
+ /** Called when custom element is added to the page. */
73
75
  connectedCallback(): void;
74
76
  /**
75
77
  * Passed into `disconnectedCallback`, since `Base` needs to run `disconnectedCallback` as well. It is overridden in each element that needs to run `disconnectedCallback`.
76
78
  */
77
79
  destroy(): void;
78
- /**
79
- * Called when custom element is removed from the page.
80
- */
80
+ /** Called when custom element is removed from the page. */
81
81
  disconnectedCallback(): void;
82
82
  }
@@ -1,3 +1,4 @@
1
+ import { Announcer } from "../announcer/index.js";
1
2
  /**
2
3
  * Each element in the library extends the `Base` class. It provides methods
3
4
  * for selecting elements via HTML attributes along with other helpers.
@@ -12,8 +13,11 @@
12
13
  */
13
14
  export class Base extends HTMLElement {
14
15
  /**
15
- * To clean up event listeners added to `document` when the element is removed.
16
+ * A single `Announcer` element to share between all drab elements to announce
17
+ * interactive changes.
16
18
  */
19
+ static #announcer = Announcer.init();
20
+ /** To clean up event listeners added to `document` when the element is removed. */
17
21
  #listenerController = new AbortController();
18
22
  constructor() {
19
23
  super();
@@ -31,6 +35,12 @@ export class Base extends HTMLElement {
31
35
  set event(value) {
32
36
  this.setAttribute("event", value);
33
37
  }
38
+ /**
39
+ * @param message message to announce to screen readers
40
+ */
41
+ announce(message) {
42
+ Base.#announcer.announce(message);
43
+ }
34
44
  /**
35
45
  * @returns All of the elements that match the `trigger` selector.
36
46
  * @default this.querySelectorAll("[data-trigger]")
@@ -40,8 +50,8 @@ export class Base extends HTMLElement {
40
50
  return triggers;
41
51
  }
42
52
  /**
43
- * @param instance The instance of the desired element, ex: `HTMLDialogElement`.
44
- * Defaults to `HTMLElement`.
53
+ * @param instance The instance of the desired element to validate against,
54
+ * ex: `HTMLDialogElement`. Defaults to `HTMLElement`.
45
55
  * @returns The element that matches the `content` selector.
46
56
  * @default this.querySelector("[data-content]")
47
57
  */
@@ -115,9 +125,7 @@ export class Base extends HTMLElement {
115
125
  * 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.
116
126
  */
117
127
  mount() { }
118
- /**
119
- * Called when custom element is added to the page.
120
- */
128
+ /** Called when custom element is added to the page. */
121
129
  connectedCallback() {
122
130
  queueMicrotask(() => this.mount());
123
131
  }
@@ -125,9 +133,7 @@ export class Base extends HTMLElement {
125
133
  * Passed into `disconnectedCallback`, since `Base` needs to run `disconnectedCallback` as well. It is overridden in each element that needs to run `disconnectedCallback`.
126
134
  */
127
135
  destroy() { }
128
- /**
129
- * Called when custom element is removed from the page.
130
- */
136
+ /** Called when custom element is removed from the page. */
131
137
  disconnectedCallback() {
132
138
  this.destroy();
133
139
  this.#listenerController.abort();
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { ContextMenu } from "./index.js";
2
- customElements.define("drab-contextmenu", ContextMenu);
3
+ define("drab-contextmenu", ContextMenu);
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Copy } from "./index.js";
2
- customElements.define("drab-copy", Copy);
3
+ define("drab-copy", Copy);
package/dist/define.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as elements from "./index.js";
2
- for (const [key, value] of Object.entries(elements)) {
3
- customElements.define(`drab-${key.toLowerCase()}`, value);
2
+ import { define } from "./util/define.js";
3
+ for (const [name, Constructor] of Object.entries(elements)) {
4
+ define(`drab-${name.toLowerCase()}`, Constructor);
4
5
  }
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Dialog } from "./index.js";
2
- customElements.define("drab-dialog", Dialog);
3
+ define("drab-dialog", Dialog);
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Editor } from "./index.js";
2
- customElements.define("drab-editor", Editor);
3
+ define("drab-editor", Editor);
@@ -1,5 +1,10 @@
1
1
  import { Base, type BaseAttributes } from "../base/index.js";
2
2
  export type EditorAttributes = BaseAttributes;
3
+ export type EditorTriggerAttributes = {
4
+ "data-value": string;
5
+ "data-key": string;
6
+ "data-type": "block" | "wrap" | "inline";
7
+ };
3
8
  /**
4
9
  * A piece of content to insert into the `textarea`.
5
10
  */
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Fullscreen } from "./index.js";
2
- customElements.define("drab-fullscreen", Fullscreen);
3
+ define("drab-fullscreen", Fullscreen);
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
- export { Base, type BaseAttributes } from "./base/index.js";
2
- export { ContextMenu, type ContextMenuAttributes, } from "./contextmenu/index.js";
3
- export { Copy, type CopyAttributes } from "./copy/index.js";
4
- export { Dialog, type DialogAttributes } from "./dialog/index.js";
5
- export { Editor, type EditorAttributes } from "./editor/index.js";
6
- export { Fullscreen, type FullscreenAttributes } from "./fullscreen/index.js";
7
- export { Intersect, type IntersectAttributes } from "./intersect/index.js";
8
- export { Prefetch, type PrefetchAttributes } from "./prefetch/index.js";
9
- export { Share, type ShareAttributes } from "./share/index.js";
10
- export { TableSort, type TableSortAttributes } from "./tablesort/index.js";
11
- export { WakeLock, type WakeLockAttributes } from "./wakelock/index.js";
12
- export { YouTube, type YouTubeAttributes } from "./youtube/index.js";
1
+ export * from "./announcer/index.js";
2
+ export * from "./base/index.js";
3
+ export * from "./contextmenu/index.js";
4
+ export * from "./copy/index.js";
5
+ export * from "./dialog/index.js";
6
+ export * from "./editor/index.js";
7
+ export * from "./fullscreen/index.js";
8
+ export * from "./intersect/index.js";
9
+ export * from "./prefetch/index.js";
10
+ export * from "./share/index.js";
11
+ export * from "./tablesort/index.js";
12
+ export * from "./wakelock/index.js";
13
+ export * from "./youtube/index.js";
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
- export { Base } from "./base/index.js";
2
- export { ContextMenu, } from "./contextmenu/index.js";
3
- export { Copy } from "./copy/index.js";
4
- export { Dialog } from "./dialog/index.js";
5
- export { Editor } from "./editor/index.js";
6
- export { Fullscreen } from "./fullscreen/index.js";
7
- export { Intersect } from "./intersect/index.js";
8
- export { Prefetch } from "./prefetch/index.js";
9
- export { Share } from "./share/index.js";
10
- export { TableSort } from "./tablesort/index.js";
11
- export { WakeLock } from "./wakelock/index.js";
12
- export { YouTube } from "./youtube/index.js";
1
+ export * from "./announcer/index.js";
2
+ export * from "./base/index.js";
3
+ export * from "./contextmenu/index.js";
4
+ export * from "./copy/index.js";
5
+ export * from "./dialog/index.js";
6
+ export * from "./editor/index.js";
7
+ export * from "./fullscreen/index.js";
8
+ export * from "./intersect/index.js";
9
+ export * from "./prefetch/index.js";
10
+ export * from "./share/index.js";
11
+ export * from "./tablesort/index.js";
12
+ export * from "./wakelock/index.js";
13
+ export * from "./youtube/index.js";
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Intersect } from "./index.js";
2
- customElements.define("drab-intersect", Intersect);
3
+ define("drab-intersect", Intersect);
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Prefetch } from "./index.js";
2
- customElements.define("drab-prefetch", Prefetch);
3
+ define("drab-prefetch", Prefetch);
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { Share } from "./index.js";
2
- customElements.define("drab-share", Share);
3
+ define("drab-share", Share);
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { TableSort } from "./index.js";
2
- customElements.define("drab-tablesort", TableSort);
3
+ define("drab-tablesort", TableSort);
@@ -1,5 +1,9 @@
1
1
  import { Base, type BaseAttributes } from "../base/index.js";
2
2
  export type TableSortAttributes = BaseAttributes;
3
+ export type TableSortTriggerAttributes = {
4
+ "data-type": "string" | "boolean" | "number";
5
+ "data-value": string;
6
+ };
3
7
  /**
4
8
  * Wrap a `HTMLTableElement` in the `TableSort` element to have sortable column
5
9
  * headers. Set each `th` that you want to sort to the `trigger`. Set the `tbody`
@@ -20,7 +20,7 @@ export class TableSort extends Base {
20
20
  * Removes `data-asc` or `data-desc` from other triggers then sets the correct attribute on the selected trigger.
21
21
  *
22
22
  * @param trigger
23
- * @returns true if ascending, false if descending
23
+ * @returns `true` if ascending, `false` if descending
24
24
  */
25
25
  #setAttributes(trigger) {
26
26
  const asc = "data-asc";
@@ -43,11 +43,24 @@ export class TableSort extends Base {
43
43
  mount() {
44
44
  const tbody = this.getContent(HTMLTableSectionElement);
45
45
  for (const trigger of this.getTrigger()) {
46
- trigger.addEventListener(this.event, () => {
46
+ trigger.tabIndex = 0;
47
+ trigger.role = "button";
48
+ const listener = () => {
49
+ const asc = this.#setAttributes(trigger);
47
50
  Array.from(tbody.querySelectorAll("tr"))
48
- .sort(comparer(trigger, this.#setAttributes(trigger)))
51
+ .sort(comparer(trigger, asc))
49
52
  .forEach((tr) => tbody.appendChild(tr));
50
- });
53
+ this.announce(`sorted table by ${trigger.textContent} in ${asc ? "ascending" : "descending"} order`);
54
+ };
55
+ trigger.addEventListener(this.event, listener);
56
+ if (this.event === "click") {
57
+ trigger.addEventListener("keydown", (e) => {
58
+ if (e.key === "Enter" || e.key === " ") {
59
+ e.preventDefault();
60
+ listener();
61
+ }
62
+ });
63
+ }
51
64
  }
52
65
  }
53
66
  }
@@ -65,23 +78,9 @@ const comparer = (th, ascending) => {
65
78
  return collator.compare(aVal, bVal);
66
79
  }
67
80
  else if (dataType === "boolean") {
68
- /**
69
- * if value is one of these and type is boolean
70
- * it should be considered falsy
71
- * since actually `Boolean("false") === true`
72
- * @param val string pulled from the textContent or attr
73
- * @returns a boolean of the provided string
74
- */
75
- const convertToBoolean = (val) => {
76
- const falsy = ["0", "false", "null", "undefined"];
77
- if (falsy.includes(val)) {
78
- return false;
79
- }
80
- return Boolean(val);
81
- };
82
- return convertToBoolean(aVal) === convertToBoolean(bVal)
81
+ return falsyBoolean(aVal) === falsyBoolean(bVal)
83
82
  ? 0
84
- : convertToBoolean(aVal)
83
+ : falsyBoolean(aVal)
85
84
  ? -1
86
85
  : 1;
87
86
  }
@@ -107,3 +106,16 @@ const getValue = (tr, i) => {
107
106
  }
108
107
  return "";
109
108
  };
109
+ /**
110
+ * if value is one of these and type is boolean
111
+ * it should be considered falsy
112
+ * since actually `Boolean("false") === true`
113
+ * @param val string pulled from the textContent or attr
114
+ * @returns a boolean of the provided string
115
+ */
116
+ const falsyBoolean = (val) => {
117
+ if (["0", "false", "null", "undefined"].includes(val)) {
118
+ return false;
119
+ }
120
+ return Boolean(val);
121
+ };
@@ -0,0 +1,8 @@
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 declare const define: (name: string, Constructor: CustomElementConstructor) => void;
@@ -0,0 +1,11 @@
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, Constructor) => {
9
+ if (!customElements.get(name))
10
+ customElements.define(name, Constructor);
11
+ };
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { WakeLock } from "./index.js";
2
- customElements.define("drab-wakelock", WakeLock);
3
+ define("drab-wakelock", WakeLock);
@@ -34,9 +34,11 @@ export class WakeLock extends Base {
34
34
  if (this.#wakeLockSupported() && document.visibilityState === "visible") {
35
35
  this.wakeLock = await navigator.wakeLock.request("screen");
36
36
  this.setAttribute("locked", "");
37
+ this.announce("screen wake lock activated");
37
38
  this.swapContent(false);
38
39
  this.wakeLock.addEventListener("release", () => {
39
40
  this.removeAttribute("locked");
41
+ this.announce("screen wake lock deactivated");
40
42
  this.swapContent(false);
41
43
  if (!this.#autoLock) {
42
44
  // set to null is required, used to determine if screen should be
@@ -1,2 +1,3 @@
1
+ import { define } from "../util/define.js";
1
2
  import { YouTube } from "./index.js";
2
- customElements.define("drab-youtube", YouTube);
3
+ define("drab-youtube", YouTube);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "drab",
3
3
  "description": "Interactivity for You",
4
- "version": "6.0.0",
4
+ "version": "6.1.0",
5
5
  "homepage": "https://drab.robino.dev",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -42,6 +42,14 @@
42
42
  "types": "./dist/define.d.ts",
43
43
  "default": "./dist/define.js"
44
44
  },
45
+ "./announcer": {
46
+ "types": "./dist/announcer/index.d.ts",
47
+ "default": "./dist/announcer/index.js"
48
+ },
49
+ "./announcer/define": {
50
+ "types": "./dist/announcer/define.d.ts",
51
+ "default": "./dist/announcer/define.js"
52
+ },
45
53
  "./base": {
46
54
  "types": "./dist/base/index.d.ts",
47
55
  "default": "./dist/base/index.js"