@ascribed/elements 0.4.4 → 0.4.5-next.34

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.
package/CONTRACT.md CHANGED
@@ -15,12 +15,13 @@ The site output (SPEC §9.4) is markdown plus custom elements. This contract is
15
15
  | A tested example's `@snippet` | [A caption line on its code block](#9-a-tested-code-block), with the class `ascribe-code-tested` | CSS only |
16
16
  | A link out of a line of a versioned build | [An ordinary link](#10-a-link-out-of-a-line), marked with `data-ascribe-release-line` | Optional CSS |
17
17
  | `@operation` | [`<ascribe-operation>`](#11-ascribe-operation) | CSS only |
18
+ | A code block | [A copy button after its `<pre>`](#12-a-code-blocks-copy-button), which the script adds | JavaScript and CSS |
18
19
 
19
20
  A change to this contract changes the site emitter (`crates/ascribe-emit`), the element library, and the site-render fixtures (`tests/render/`) together.
20
21
 
21
22
  ## 0. Rules for every element
22
23
 
23
- **Light DOM, CSS, no behavior.** Elements render into the light DOM, so site styles apply and content stays visible to search engines and assistive technology (SPEC §9.7). They're styled with CSS and themed with custom properties whose names start with `--ascribe-`; the package README lists them. The names are part of this contract; their default values aren't, and a release may change them, noting it in the changelog. Only `<ascribe-tabs>` runs JavaScript. Everything else must render meaningfully with the library's CSS and no script, and must stay readable with neither.
24
+ **Light DOM, CSS, no behavior.** Elements render into the light DOM, so site styles apply and content stays visible to search engines and assistive technology (SPEC §9.7). They're styled with CSS and themed with custom properties whose names start with `--ascribe-`; the package README lists them. The names are part of this contract; their default values aren't, and a release may change them, noting it in the changelog. Only `<ascribe-tabs>`, and the copy button the script adds to code blocks (§12), run JavaScript. Everything else must render meaningfully with the library's CSS and no script, and must stay readable with neither.
24
25
 
25
26
  **How the emitter writes elements.** Custom elements are raw HTML blocks in CommonMark, so the layout matters:
26
27
 
@@ -98,13 +99,15 @@ A `@variant` group that keeps more than one arm in the build (SPEC §9.3, §9.4)
98
99
  | `value` | dimensional arms only | The arm's values for the group's `sync` dimension, separated by single spaces: `npm`, or `npm pnpm` for an arm written `{pm=npm\|pnpm}`. Dimension values are names without spaces, so the list is unambiguous. |
99
100
  | `label` | yes | What the tab is called. For a labeled arm, the plain text of its title (§0). For a dimensional arm, each of its attributes, in canonical order, as the labels of its values joined by ` / `, with attributes joined by `, `: `npm`, `Quill Cloud, npm`, `npm / pnpm`. |
100
101
 
101
- **Children:** the arm's content, as markdown blocks.
102
+ **Children:** first, the tab's `label` as text, a paragraph of its own: `<p class="ascribe-tab-label">`, holding the label escaped as an attribute value is (§0), as one HTML block. Then the arm's content, as markdown blocks. The paragraph is there for a reader of the HTML that runs neither the script nor the stylesheet, such as an HTML-to-text or HTML-to-Markdown converter, which then finds each tab's label before its content ([decision #1038](../../project-docs/decisions/1038-tab-labels-and-code-languages.md)).
102
103
 
103
104
  ````html
104
105
  <ascribe-tabs sync="pm">
105
106
 
106
107
  <ascribe-tab value="npm" label="npm">
107
108
 
109
+ <p class="ascribe-tab-label">npm</p>
110
+
108
111
  ```shell
109
112
  npm install -g @quill/agent
110
113
  ```
@@ -113,6 +116,8 @@ npm install -g @quill/agent
113
116
 
114
117
  <ascribe-tab value="pnpm" label="pnpm">
115
118
 
119
+ <p class="ascribe-tab-label">pnpm</p>
120
+
116
121
  ```shell
117
122
  pnpm add -g @quill/agent
118
123
  ```
@@ -124,11 +129,12 @@ pnpm add -g @quill/agent
124
129
 
125
130
  ### Rendering
126
131
 
127
- **Without JavaScript** every tab shows, one after another, each introduced by its `label` (generated by CSS), so no content is hidden (SPEC §9.7).
132
+ **Without JavaScript** every tab shows, one after another, each introduced by its label paragraph, so no content is hidden (SPEC §9.7).
128
133
 
129
134
  **With JavaScript** the element follows the WAI-ARIA Authoring Practices tabs pattern:
130
135
 
131
136
  - It inserts a tab list as its first child: an element with `role="tablist"` holding one `<button role="tab">` per tab, whose text is the tab's `label`. Each `<ascribe-tab>` gets `role="tabpanel"`, an `id` if it has none, and `aria-labelledby` pointing at its button; the buttons get `aria-controls`, `aria-selected`, and a roving `tabindex`.
137
+ - Each tab's label paragraph gets `aria-hidden="true"`, so a screen reader hears the label once, from the tab's button, and the stylesheet puts it out of sight, visually hidden as the pointer for agents is (§8): not `display: none`, so a reader that takes the page's rendered text still finds it.
132
138
  - One tab shows at a time; the others get the `hidden` attribute.
133
139
  - Left and Right arrows move between tabs and select them, wrapping at the ends; Home and End go to the first and last tab.
134
140
  - **Initial selection:** for a group with `sync`, the first tab whose `value` contains the remembered value for that dimension; otherwise, and for labeled groups, the first tab.
@@ -316,3 +322,17 @@ An operation of a declared API (SPEC §4.13, §7.11), from an `@operation` whose
316
322
  ```
317
323
 
318
324
  **Rendering, with or without JavaScript:** a block holding the line, a deprecated operation's link struck through and its "(deprecated)" in the deprecated state's color. A site styles the method or the path by selecting on the attributes (`ascribe-operation[method="delete"]`). With no CSS, it's the link and its words: the deprecation is in the text, never only in the styling.
325
+
326
+ ## 12. A code block's copy button
327
+
328
+ There's no element for a code block, and the emitter writes nothing for this: the library's script adds a button to each code block in the page, a `<pre>` with a `<code>` child, as the site output's fenced code blocks render, including one in a figure with a title or a tested line (§9) and one in a tab (§3). It copies the code ([decision #1011](../../project-docs/decisions/1011-copy-button.md)).
329
+
330
+ - **Where it goes:** the button is a `<button type="button" class="ascribe-copy">`, inserted directly after the `<pre>`, as its next sibling: outside the `<pre>` and its `<code>`, so their text, and an HTML-to-text converter's, is the code alone. It has no text of its own; its words are the stylesheet's (below).
331
+ - **What it copies:** the `<code>` element's text, without the newline that ends the block, and nothing else: not a figure's caption, not the button.
332
+ - **Naming and saying:** its accessible name, `aria-label`, is the stylesheet's word for it when it's made, "Copy". After a click it carries `data-ascribe-copy="copied"`, or `"failed"` when the clipboard refuses, for two seconds, and the page's one live region, a `<div class="ascribe-copy-status" role="status">` the script adds to the end of `<body>`, says the stylesheet's words for that state, "Copied" or "Couldn't copy", then empties.
333
+ - **Placement:** the script gives each `<pre>` an `anchor-name` of its own and the button the same `position-anchor`, in their `style`, and the stylesheet puts the button in the block's top corner, at its inline end. A browser without CSS anchor positioning shows it under the block, at its end.
334
+ - **Code blocks added later,** by a script, or a page swapped in place, get a button too.
335
+
336
+ **Without JavaScript** there's no button, and the page reads as it does with no element library: the script adds the button, and the stylesheet doesn't. **Without a clipboard,** on a page that isn't a secure context, the script adds none either.
337
+
338
+ **Theming:** `--ascribe-copy-display` is the button's `display`, `block` by default; a site sets it to `none` to leave the button out, on `:root` or on any element around the blocks it means. Its colors are `--ascribe-copy-color`, `--ascribe-copy-background`, `--ascribe-copy-hover-background`, and `--ascribe-copy-focus-color`. Its words are generated content, `.ascribe-copy::before`, and `.ascribe-copy[data-ascribe-copy="copied"]::before` and `[data-ascribe-copy="failed"]` for its two states, so a site restyles or translates them once, as it does "Available:", and the button's name and the live region follow. It isn't printed.
package/README.md CHANGED
@@ -6,8 +6,9 @@ The custom elements [Ascribe](https://github.com/ascribed-dev/ascribe)'s site ou
6
6
  `<ascribe-availability-target>`, `<ascribe-group>`, and `<ascribe-for-agents>`.
7
7
 
8
8
  - Elements render into the light DOM and are styled by CSS.
9
- - Only `<ascribe-tabs>` uses JavaScript. Without the script, every tab shows
10
- with its label; nothing is hidden.
9
+ - Only `<ascribe-tabs>` uses JavaScript, and the script adds a copy button to
10
+ each code block. Without the script, every tab shows with its label, nothing
11
+ is hidden, and there's no copy button.
11
12
 
12
13
  An Astro site gets them through `@ascribed/astro`, whose `<Elements />`
13
14
  component loads the stylesheet and the script.
@@ -15,7 +16,8 @@ component loads the stylesheet and the script.
15
16
  ## Using it
16
17
 
17
18
  ```js
18
- // Registers <ascribe-tabs>, <ascribe-tab>, and <ascribe-group>.
19
+ // Registers <ascribe-tabs>, <ascribe-tab>, and <ascribe-group>, and adds
20
+ // the copy buttons.
19
21
  import "@ascribed/elements";
20
22
  // The styles. Import this alone to style the elements without the script.
21
23
  import "@ascribed/elements/style.css";
package/css/style.css CHANGED
@@ -1,9 +1,10 @@
1
1
  /*
2
2
  * @ascribed/elements: styles for the Ascribe site output (CONTRACT.md).
3
3
  *
4
- * Everything here works without the script; only <ascribe-tabs> adds
5
- * behavior. Theme it by setting the --ascribe-* custom properties (listed in
6
- * README.md) on :root or any ancestor.
4
+ * Everything here works without the script; only <ascribe-tabs>, and the copy
5
+ * button the script adds to code blocks, have behavior. Theme it by setting
6
+ * the --ascribe-* custom properties (listed in README.md) on :root or any
7
+ * ancestor.
7
8
  *
8
9
  * The colors are light or dark as the page is: dark where the page's
9
10
  * `color-scheme` is `dark`, or `light dark` with the reader's system in dark
@@ -63,6 +64,15 @@
63
64
  --ascribe-tab-hover-background: #e1ebf1;
64
65
  /* End of generated tabs. */
65
66
 
67
+ /* The copy button on code blocks: `none` leaves it out. */
68
+ --ascribe-copy-display: block;
69
+ /* Generated from design/tokens.toml by scripts/design/tokens.ts: copy, light. */
70
+ --ascribe-copy-color: #47606f;
71
+ --ascribe-copy-background: #ffffff;
72
+ --ascribe-copy-hover-background: #e1ebf1;
73
+ --ascribe-copy-focus-color: #b4530f;
74
+ /* End of generated copy. */
75
+
66
76
  /* Availability lifecycle states: an accent color and a background each. */
67
77
  /* Generated from design/tokens.toml by scripts/design/tokens.ts: states, light. */
68
78
  --ascribe-state-color: #47606f;
@@ -118,6 +128,13 @@
118
128
  --ascribe-tab-hover-background: light-dark(#e1ebf1, #1f303e);
119
129
  /* End of generated tabs. */
120
130
 
131
+ /* Generated from design/tokens.toml by scripts/design/tokens.ts: copy, light-dark. */
132
+ --ascribe-copy-color: light-dark(#47606f, #8aa6b8);
133
+ --ascribe-copy-background: light-dark(#ffffff, #112233);
134
+ --ascribe-copy-hover-background: light-dark(#e1ebf1, #1f303e);
135
+ --ascribe-copy-focus-color: light-dark(#b4530f, #f0a35e);
136
+ /* End of generated copy. */
137
+
121
138
  /* Generated from design/tokens.toml by scripts/design/tokens.ts: states, light-dark. */
122
139
  --ascribe-state-color: light-dark(#47606f, #8aa6b8);
123
140
  --ascribe-state-background: light-dark(#deeaf0, #1f303e);
@@ -136,12 +153,26 @@
136
153
 
137
154
  /* A page that says which it is. */
138
155
  :root[data-ascribe-scheme="light"]
139
- :is(ascribe-note, ascribe-steps, ascribe-tabs, ascribe-availability, ascribe-operation) {
156
+ :is(
157
+ ascribe-note,
158
+ ascribe-steps,
159
+ ascribe-tabs,
160
+ ascribe-availability,
161
+ ascribe-operation,
162
+ .ascribe-copy
163
+ ) {
140
164
  color-scheme: light;
141
165
  }
142
166
 
143
167
  :root[data-ascribe-scheme="dark"]
144
- :is(ascribe-note, ascribe-steps, ascribe-tabs, ascribe-availability, ascribe-operation) {
168
+ :is(
169
+ ascribe-note,
170
+ ascribe-steps,
171
+ ascribe-tabs,
172
+ ascribe-availability,
173
+ ascribe-operation,
174
+ .ascribe-copy
175
+ ) {
145
176
  color-scheme: dark;
146
177
  }
147
178
  }
@@ -268,20 +299,15 @@ ascribe-tab[hidden] {
268
299
  display: none;
269
300
  }
270
301
 
271
- /* Without the script: every tab shows, introduced by its label. */
272
- ascribe-tab::before {
273
- content: attr(label);
274
- display: block;
275
- margin-top: var(--ascribe-space);
302
+ /* Without the script: every tab shows, introduced by its label, which is
303
+ text in the page, its first paragraph. With it, the tab list carries the
304
+ labels, and the paragraph is out of sight (below). */
305
+ .ascribe-tab-label {
306
+ margin-block: var(--ascribe-space) 0;
276
307
  color: var(--ascribe-muted-color);
277
308
  font-weight: var(--ascribe-font-weight-strong);
278
309
  }
279
310
 
280
- /* With the script: the tab list carries the labels. */
281
- ascribe-tab[role="tabpanel"]::before {
282
- display: none;
283
- }
284
-
285
311
  ascribe-tab[role="tabpanel"] {
286
312
  padding-top: var(--ascribe-space-small);
287
313
  }
@@ -439,14 +465,79 @@ ascribe-group {
439
465
  color: var(--ascribe-muted-color);
440
466
  }
441
467
 
468
+ /* A code block's copy button (CONTRACT.md §12) -------------------------- */
469
+
470
+ /* The script adds the button after a code block's <pre>, so with no script
471
+ there's none. Its words are generated content: a site restyles or
472
+ translates them here, and they're never in the page's text, nor in what a
473
+ reader copies. A site sets --ascribe-copy-display to `none` to leave the
474
+ button out. Where the browser can anchor it, it sits in the block's top
475
+ corner; elsewhere it's under the block, at its end. */
476
+ .ascribe-copy {
477
+ display: var(--ascribe-copy-display);
478
+ inline-size: fit-content;
479
+ margin-inline-start: auto;
480
+ padding: 0.125rem var(--ascribe-space-small);
481
+ border: var(--ascribe-border-width) solid var(--ascribe-border-color);
482
+ border-radius: var(--ascribe-radius);
483
+ background: var(--ascribe-copy-background);
484
+ color: var(--ascribe-copy-color);
485
+ font-family: var(--ascribe-font-family);
486
+ font-size: var(--ascribe-font-size-small);
487
+ line-height: 1.5;
488
+ cursor: pointer;
489
+ }
490
+
491
+ .ascribe-copy::before {
492
+ content: "Copy";
493
+ }
494
+
495
+ .ascribe-copy[data-ascribe-copy="copied"]::before {
496
+ content: "Copied";
497
+ }
498
+
499
+ .ascribe-copy[data-ascribe-copy="failed"]::before {
500
+ content: "Couldn't copy";
501
+ }
502
+
503
+ .ascribe-copy:hover {
504
+ background: var(--ascribe-copy-hover-background);
505
+ }
506
+
507
+ .ascribe-copy:focus-visible {
508
+ outline: 2px solid var(--ascribe-copy-focus-color);
509
+ outline-offset: 2px;
510
+ }
511
+
512
+ /* The script names each code block as an anchor, and its button's
513
+ `position-anchor` is that name. */
514
+ @supports (anchor-name: --a) {
515
+ .ascribe-copy {
516
+ position: absolute;
517
+ inset-block-start: calc(anchor(start) + var(--ascribe-space-small) / 2);
518
+ inset-inline-end: calc(anchor(end) + var(--ascribe-space-small) / 2);
519
+ margin: 0;
520
+ }
521
+ }
522
+
523
+ @media print {
524
+ .ascribe-copy {
525
+ display: none;
526
+ }
527
+ }
528
+
442
529
  /* <ascribe-for-agents> --------------------------------------------------- */
443
530
 
444
531
  /* The pointer to llms.txt: out of sight, but in the page for an agent (and a
445
532
  screen reader) to read. Not `display: none`, which some readers skip. A
446
533
  table row's "(Available: " and ")" around its badges are hidden the same
447
- way, so a screen reader says what the badges are, as the Markdown does. */
534
+ way, so a screen reader says what the badges are, as the Markdown does,
535
+ and so is what a copy button says when it has copied, and a tab's label
536
+ once the tab list shows it, for a reader that takes the page's text. */
448
537
  ascribe-for-agents,
449
- .ascribe-availability-lead-in {
538
+ ascribe-tab[role="tabpanel"] > .ascribe-tab-label,
539
+ .ascribe-availability-lead-in,
540
+ .ascribe-copy-status {
450
541
  position: absolute;
451
542
  width: 1px;
452
543
  height: 1px;
package/dist/copy.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Add copy buttons to the page's code blocks, now and as they're added. */
2
+ export declare function startCopyButtons(): void;
package/dist/copy.js ADDED
@@ -0,0 +1,151 @@
1
+ // The copy button on code blocks (CONTRACT.md §12). The script adds a
2
+ // <button> after each code block's <pre>, outside it, so the code's text, and
3
+ // a copy of it, never holds the button. The button has no text of its own:
4
+ // its words, "Copy", "Copied", and "Couldn't copy", are the stylesheet's
5
+ // generated content, and the script reads them from there for the button's
6
+ // name and for the visually hidden live region that says a click's outcome.
7
+ // Each block is an anchor of its own, with a name the script gives it, so
8
+ // the stylesheet can put the button in the block's corner. Without the
9
+ // script there's no button, and without a clipboard (a page that isn't a
10
+ // secure context) the script adds none.
11
+ import { CLASS_COPY, CLASS_COPY_STATUS, DATA_COPY } from "./names.js";
12
+ /** How long a button says "Copied" or "Couldn't copy" before it says "Copy" again. */
13
+ const SHOWN_MS = 2000;
14
+ /** What a button says when the stylesheet gives it no words. */
15
+ const FALLBACK = { copy: "Copy", copied: "Copied", failed: "Couldn't copy" };
16
+ const timers = new WeakMap();
17
+ /** The anchor name each block was given; a copy of a block (review's) has none here. */
18
+ const anchors = new WeakMap();
19
+ let anchorCount = 0;
20
+ let status = null;
21
+ /** The code element of a code block: a `<pre>` whose child is a `<code>`. */
22
+ function codeOf(element) {
23
+ if (element.localName !== "pre")
24
+ return null;
25
+ return Array.from(element.children).find((child) => child.localName === "code") ?? null;
26
+ }
27
+ /**
28
+ * Give `pre` its button, unless it's not a code block or already has one, and
29
+ * an anchor name of its own for the button. Returns a button it made.
30
+ */
31
+ function addButton(pre) {
32
+ if (codeOf(pre) === null || anchors.has(pre))
33
+ return undefined;
34
+ const name = `--ascribe-code-${++anchorCount}`;
35
+ anchors.set(pre, name);
36
+ pre.style.setProperty("anchor-name", name);
37
+ // A copy of a block whose button was copied with it keeps that button.
38
+ const next = pre.nextElementSibling;
39
+ if (next instanceof HTMLButtonElement && next.classList.contains(CLASS_COPY)) {
40
+ next.style.setProperty("position-anchor", name);
41
+ return undefined;
42
+ }
43
+ const button = document.createElement("button");
44
+ button.type = "button";
45
+ button.className = CLASS_COPY;
46
+ button.style.setProperty("position-anchor", name);
47
+ pre.after(button);
48
+ return button;
49
+ }
50
+ /** Give every code block in `root`, and `root` itself, a button. */
51
+ function addButtons(root) {
52
+ const made = [root, ...Array.from(root.querySelectorAll("pre"))]
53
+ .map(addButton)
54
+ .filter((button) => button !== undefined);
55
+ // Named after they're all in, so the page's style is worked out once.
56
+ for (const button of made)
57
+ button.setAttribute("aria-label", wordsOn(button) || FALLBACK.copy);
58
+ }
59
+ /** The code block a button copies: the nearest `<pre>` before it. */
60
+ function blockOf(button) {
61
+ for (let el = button.previousElementSibling; el !== null; el = el.previousElementSibling) {
62
+ if (codeOf(el) !== null)
63
+ return el;
64
+ }
65
+ return null;
66
+ }
67
+ /** The live region, made once, the first time it's needed. */
68
+ function liveRegion() {
69
+ if (status === null || !status.isConnected) {
70
+ status = document.createElement("div");
71
+ status.className = CLASS_COPY_STATUS;
72
+ status.setAttribute("role", "status");
73
+ document.body.append(status);
74
+ }
75
+ return status;
76
+ }
77
+ /** The words the stylesheet shows on a button, or none. */
78
+ function wordsOn(button) {
79
+ const content = getComputedStyle(button, "::before").content;
80
+ // One string or several (`"Copied" "!"`), as engines serialize `content`.
81
+ const parts = content.match(/"(?:[^"\\]|\\.)*"/g) ?? [];
82
+ return parts.map((part) => part.slice(1, -1).replace(/\\(.)/g, "$1")).join("");
83
+ }
84
+ /** Show `outcome` on the button for a moment, and say it in the live region. */
85
+ function show(button, outcome) {
86
+ const region = liveRegion();
87
+ button.setAttribute(DATA_COPY, outcome);
88
+ // Empty first, so the same words a second time are announced again.
89
+ region.textContent = "";
90
+ region.textContent = wordsOn(button) || FALLBACK[outcome];
91
+ clearTimeout(timers.get(button));
92
+ timers.set(button, setTimeout(() => {
93
+ button.removeAttribute(DATA_COPY);
94
+ if (region.textContent !== "")
95
+ region.textContent = "";
96
+ }, SHOWN_MS));
97
+ }
98
+ async function copy(button) {
99
+ const block = blockOf(button);
100
+ const code = block === null ? null : codeOf(block);
101
+ if (code === null)
102
+ return;
103
+ // The code as written, without the newline that ends the block, so a
104
+ // pasted command doesn't run before the reader has read it.
105
+ const text = (code.textContent ?? "").replace(/\n+$/, "");
106
+ try {
107
+ await navigator.clipboard.writeText(text);
108
+ show(button, "copied");
109
+ }
110
+ catch {
111
+ show(button, "failed");
112
+ }
113
+ }
114
+ function onClick(event) {
115
+ const target = event.target;
116
+ if (!(target instanceof Element))
117
+ return;
118
+ const button = target.closest(`button.${CLASS_COPY}`);
119
+ if (button !== null)
120
+ void copy(button);
121
+ }
122
+ function setup() {
123
+ addButtons(document.body);
124
+ // Code blocks a page adds later, or a new body: a preview that redraws, a
125
+ // site that swaps pages in place.
126
+ new MutationObserver((records) => {
127
+ for (const record of records) {
128
+ for (const node of Array.from(record.addedNodes)) {
129
+ if (node instanceof Element)
130
+ addButtons(node);
131
+ }
132
+ }
133
+ }).observe(document.documentElement, { childList: true, subtree: true });
134
+ }
135
+ let started = false;
136
+ /** Add copy buttons to the page's code blocks, now and as they're added. */
137
+ export function startCopyButtons() {
138
+ if (started || typeof document === "undefined")
139
+ return;
140
+ started = true;
141
+ if (typeof navigator.clipboard?.writeText !== "function")
142
+ return;
143
+ document.addEventListener("click", onClick);
144
+ if (document.readyState === "loading") {
145
+ document.addEventListener("DOMContentLoaded", setup, { once: true });
146
+ }
147
+ else {
148
+ setup();
149
+ }
150
+ }
151
+ //# sourceMappingURL=copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy.js","sourceRoot":"","sources":["../src/copy.ts"],"names":[],"mappings":"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","sourcesContent":["// The copy button on code blocks (CONTRACT.md §12). The script adds a\n// <button> after each code block's <pre>, outside it, so the code's text, and\n// a copy of it, never holds the button. The button has no text of its own:\n// its words, \"Copy\", \"Copied\", and \"Couldn't copy\", are the stylesheet's\n// generated content, and the script reads them from there for the button's\n// name and for the visually hidden live region that says a click's outcome.\n// Each block is an anchor of its own, with a name the script gives it, so\n// the stylesheet can put the button in the block's corner. Without the\n// script there's no button, and without a clipboard (a page that isn't a\n// secure context) the script adds none.\n\nimport { CLASS_COPY, CLASS_COPY_STATUS, DATA_COPY } from \"./names.js\";\n\n/** How long a button says \"Copied\" or \"Couldn't copy\" before it says \"Copy\" again. */\nconst SHOWN_MS = 2000;\n\n/** What a button says when the stylesheet gives it no words. */\nconst FALLBACK = { copy: \"Copy\", copied: \"Copied\", failed: \"Couldn't copy\" } as const;\n\ntype Outcome = \"copied\" | \"failed\";\n\nconst timers = new WeakMap<Element, ReturnType<typeof setTimeout>>();\n\n/** The anchor name each block was given; a copy of a block (review's) has none here. */\nconst anchors = new WeakMap<Element, string>();\n\nlet anchorCount = 0;\n\nlet status: HTMLElement | null = null;\n\n/** The code element of a code block: a `<pre>` whose child is a `<code>`. */\nfunction codeOf(element: Element): Element | null {\n if (element.localName !== \"pre\") return null;\n return Array.from(element.children).find((child) => child.localName === \"code\") ?? null;\n}\n\n/**\n * Give `pre` its button, unless it's not a code block or already has one, and\n * an anchor name of its own for the button. Returns a button it made.\n */\nfunction addButton(pre: Element): HTMLButtonElement | undefined {\n if (codeOf(pre) === null || anchors.has(pre)) return undefined;\n const name = `--ascribe-code-${++anchorCount}`;\n anchors.set(pre, name);\n (pre as HTMLElement).style.setProperty(\"anchor-name\", name);\n // A copy of a block whose button was copied with it keeps that button.\n const next = pre.nextElementSibling;\n if (next instanceof HTMLButtonElement && next.classList.contains(CLASS_COPY)) {\n next.style.setProperty(\"position-anchor\", name);\n return undefined;\n }\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = CLASS_COPY;\n button.style.setProperty(\"position-anchor\", name);\n pre.after(button);\n return button;\n}\n\n/** Give every code block in `root`, and `root` itself, a button. */\nfunction addButtons(root: Element): void {\n const made = [root, ...Array.from(root.querySelectorAll(\"pre\"))]\n .map(addButton)\n .filter((button) => button !== undefined);\n // Named after they're all in, so the page's style is worked out once.\n for (const button of made) button.setAttribute(\"aria-label\", wordsOn(button) || FALLBACK.copy);\n}\n\n/** The code block a button copies: the nearest `<pre>` before it. */\nfunction blockOf(button: Element): Element | null {\n for (let el = button.previousElementSibling; el !== null; el = el.previousElementSibling) {\n if (codeOf(el) !== null) return el;\n }\n return null;\n}\n\n/** The live region, made once, the first time it's needed. */\nfunction liveRegion(): HTMLElement {\n if (status === null || !status.isConnected) {\n status = document.createElement(\"div\");\n status.className = CLASS_COPY_STATUS;\n status.setAttribute(\"role\", \"status\");\n document.body.append(status);\n }\n return status;\n}\n\n/** The words the stylesheet shows on a button, or none. */\nfunction wordsOn(button: Element): string {\n const content = getComputedStyle(button, \"::before\").content;\n // One string or several (`\"Copied\" \"!\"`), as engines serialize `content`.\n const parts = content.match(/\"(?:[^\"\\\\]|\\\\.)*\"/g) ?? [];\n return parts.map((part) => part.slice(1, -1).replace(/\\\\(.)/g, \"$1\")).join(\"\");\n}\n\n/** Show `outcome` on the button for a moment, and say it in the live region. */\nfunction show(button: Element, outcome: Outcome): void {\n const region = liveRegion();\n button.setAttribute(DATA_COPY, outcome);\n // Empty first, so the same words a second time are announced again.\n region.textContent = \"\";\n region.textContent = wordsOn(button) || FALLBACK[outcome];\n clearTimeout(timers.get(button));\n timers.set(\n button,\n setTimeout(() => {\n button.removeAttribute(DATA_COPY);\n if (region.textContent !== \"\") region.textContent = \"\";\n }, SHOWN_MS),\n );\n}\n\nasync function copy(button: Element): Promise<void> {\n const block = blockOf(button);\n const code = block === null ? null : codeOf(block);\n if (code === null) return;\n // The code as written, without the newline that ends the block, so a\n // pasted command doesn't run before the reader has read it.\n const text = (code.textContent ?? \"\").replace(/\\n+$/, \"\");\n try {\n await navigator.clipboard.writeText(text);\n show(button, \"copied\");\n } catch {\n show(button, \"failed\");\n }\n}\n\nfunction onClick(event: MouseEvent): void {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const button = target.closest(`button.${CLASS_COPY}`);\n if (button !== null) void copy(button);\n}\n\nfunction setup(): void {\n addButtons(document.body);\n // Code blocks a page adds later, or a new body: a preview that redraws, a\n // site that swaps pages in place.\n new MutationObserver((records) => {\n for (const record of records) {\n for (const node of Array.from(record.addedNodes)) {\n if (node instanceof Element) addButtons(node);\n }\n }\n }).observe(document.documentElement, { childList: true, subtree: true });\n}\n\nlet started = false;\n\n/** Add copy buttons to the page's code blocks, now and as they're added. */\nexport function startCopyButtons(): void {\n if (started || typeof document === \"undefined\") return;\n started = true;\n if (typeof navigator.clipboard?.writeText !== \"function\") return;\n document.addEventListener(\"click\", onClick);\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", setup, { once: true });\n } else {\n setup();\n }\n}\n"]}
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- // @ascribed/elements: registers Ascribe's custom elements. Only
2
- // <ascribe-tabs> has behavior; the rest are styled by the CSS file
1
+ // @ascribed/elements: registers Ascribe's custom elements, and adds a copy
2
+ // button to each code block. Only <ascribe-tabs> and the copy buttons have
3
+ // behavior; the rest are styled by the CSS file
3
4
  // (`@ascribed/elements/style.css`), which works without this script.
5
+ import { startCopyButtons } from "./copy.js";
4
6
  import { AscribeGroup } from "./group.js";
5
7
  import { AscribeTab, AscribeTabs, choose } from "./tabs.js";
6
8
  import { ELEMENT_GROUP, ELEMENT_TAB, ELEMENT_TABS } from "./names.js";
@@ -14,4 +16,5 @@ for (const [name, constructor] of registry) {
14
16
  if (!customElements.get(name))
15
17
  customElements.define(name, constructor);
16
18
  }
19
+ startCopyButtons();
17
20
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,mEAAmE;AACnE,qEAAqE;AAErE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;AAEzD,MAAM,QAAQ,GAAyC;IACrD,CAAC,YAAY,EAAE,WAAW,CAAC;IAC3B,CAAC,WAAW,EAAE,UAAU,CAAC;IACzB,CAAC,aAAa,EAAE,YAAY,CAAC;CAC9B,CAAC;AAEF,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,QAAQ,EAAE,CAAC;IAC3C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["// @ascribed/elements: registers Ascribe's custom elements. Only\n// <ascribe-tabs> has behavior; the rest are styled by the CSS file\n// (`@ascribed/elements/style.css`), which works without this script.\n\nimport { AscribeGroup } from \"./group.js\";\nimport { AscribeTab, AscribeTabs, choose } from \"./tabs.js\";\nimport { ELEMENT_GROUP, ELEMENT_TAB, ELEMENT_TABS } from \"./names.js\";\n\nexport { AscribeGroup, AscribeTab, AscribeTabs, choose };\n\nconst registry: [string, CustomElementConstructor][] = [\n [ELEMENT_TABS, AscribeTabs],\n [ELEMENT_TAB, AscribeTab],\n [ELEMENT_GROUP, AscribeGroup],\n];\n\nfor (const [name, constructor] of registry) {\n if (!customElements.get(name)) customElements.define(name, constructor);\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,2EAA2E;AAC3E,gDAAgD;AAChD,qEAAqE;AAErE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;AAEzD,MAAM,QAAQ,GAAyC;IACrD,CAAC,YAAY,EAAE,WAAW,CAAC;IAC3B,CAAC,WAAW,EAAE,UAAU,CAAC;IACzB,CAAC,aAAa,EAAE,YAAY,CAAC;CAC9B,CAAC;AAEF,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,QAAQ,EAAE,CAAC;IAC3C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AAC1E,CAAC;AAED,gBAAgB,EAAE,CAAC","sourcesContent":["// @ascribed/elements: registers Ascribe's custom elements, and adds a copy\n// button to each code block. Only <ascribe-tabs> and the copy buttons have\n// behavior; the rest are styled by the CSS file\n// (`@ascribed/elements/style.css`), which works without this script.\n\nimport { startCopyButtons } from \"./copy.js\";\nimport { AscribeGroup } from \"./group.js\";\nimport { AscribeTab, AscribeTabs, choose } from \"./tabs.js\";\nimport { ELEMENT_GROUP, ELEMENT_TAB, ELEMENT_TABS } from \"./names.js\";\n\nexport { AscribeGroup, AscribeTab, AscribeTabs, choose };\n\nconst registry: [string, CustomElementConstructor][] = [\n [ELEMENT_TABS, AscribeTabs],\n [ELEMENT_TAB, AscribeTab],\n [ELEMENT_GROUP, AscribeGroup],\n];\n\nfor (const [name, constructor] of registry) {\n if (!customElements.get(name)) customElements.define(name, constructor);\n}\n\nstartCopyButtons();\n"]}
package/dist/names.d.ts CHANGED
@@ -6,6 +6,11 @@ export declare const ELEMENT_STEPS = "ascribe-steps";
6
6
  export declare const ELEMENT_TABS = "ascribe-tabs";
7
7
  /** `<ascribe-tab>`: one arm of `<ascribe-tabs>` (packages/elements/CONTRACT.md §3). */
8
8
  export declare const ELEMENT_TAB = "ascribe-tab";
9
+ /**
10
+ * On the paragraph that starts an `<ascribe-tab>` and holds its label as
11
+ * text, for a reader of the HTML with no script (packages/elements/CONTRACT.md §3).
12
+ */
13
+ export declare const CLASS_TAB_LABEL = "ascribe-tab-label";
9
14
  /** `<ascribe-availability>`: a badge or filter (packages/elements/CONTRACT.md §4). */
10
15
  export declare const ELEMENT_AVAILABILITY = "ascribe-availability";
11
16
  /**
@@ -24,6 +29,22 @@ export declare const CLASS_AVAILABILITY_LEAD_IN = "ascribe-availability-lead-in"
24
29
  * (packages/elements/CONTRACT.md §9).
25
30
  */
26
31
  export declare const CLASS_CODE_TESTED = "ascribe-code-tested";
32
+ /**
33
+ * On the button the element library's script adds after a code block's
34
+ * `<pre>`, which copies its code; its words are the stylesheet's
35
+ * (packages/elements/CONTRACT.md §12).
36
+ */
37
+ export declare const CLASS_COPY = "ascribe-copy";
38
+ /**
39
+ * On a copy button for a moment after a click: `copied`, or `failed`
40
+ * when the clipboard refused (packages/elements/CONTRACT.md §12).
41
+ */
42
+ export declare const DATA_COPY = "data-ascribe-copy";
43
+ /**
44
+ * The visually hidden live region that says a copy button copied
45
+ * (packages/elements/CONTRACT.md §12).
46
+ */
47
+ export declare const CLASS_COPY_STATUS = "ascribe-copy-status";
27
48
  /**
28
49
  * `<ascribe-operation>`: an operation of a declared API, from `@operation`
29
50
  * (packages/elements/CONTRACT.md §11).
package/dist/names.js CHANGED
@@ -9,6 +9,11 @@ export const ELEMENT_STEPS = "ascribe-steps";
9
9
  export const ELEMENT_TABS = "ascribe-tabs";
10
10
  /** `<ascribe-tab>`: one arm of `<ascribe-tabs>` (packages/elements/CONTRACT.md §3). */
11
11
  export const ELEMENT_TAB = "ascribe-tab";
12
+ /**
13
+ * On the paragraph that starts an `<ascribe-tab>` and holds its label as
14
+ * text, for a reader of the HTML with no script (packages/elements/CONTRACT.md §3).
15
+ */
16
+ export const CLASS_TAB_LABEL = "ascribe-tab-label";
12
17
  /** `<ascribe-availability>`: a badge or filter (packages/elements/CONTRACT.md §4). */
13
18
  export const ELEMENT_AVAILABILITY = "ascribe-availability";
14
19
  /**
@@ -27,6 +32,22 @@ export const CLASS_AVAILABILITY_LEAD_IN = "ascribe-availability-lead-in";
27
32
  * (packages/elements/CONTRACT.md §9).
28
33
  */
29
34
  export const CLASS_CODE_TESTED = "ascribe-code-tested";
35
+ /**
36
+ * On the button the element library's script adds after a code block's
37
+ * `<pre>`, which copies its code; its words are the stylesheet's
38
+ * (packages/elements/CONTRACT.md §12).
39
+ */
40
+ export const CLASS_COPY = "ascribe-copy";
41
+ /**
42
+ * On a copy button for a moment after a click: `copied`, or `failed`
43
+ * when the clipboard refused (packages/elements/CONTRACT.md §12).
44
+ */
45
+ export const DATA_COPY = "data-ascribe-copy";
46
+ /**
47
+ * The visually hidden live region that says a copy button copied
48
+ * (packages/elements/CONTRACT.md §12).
49
+ */
50
+ export const CLASS_COPY_STATUS = "ascribe-copy-status";
30
51
  /**
31
52
  * `<ascribe-operation>`: an operation of a declared API, from `@operation`
32
53
  * (packages/elements/CONTRACT.md §11).
package/dist/names.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"names.js","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"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","sourcesContent":["// The names Ascribe puts on a page: elements, attributes, classes, and ids.\n// Generated from crates/ascribe-core/src/names.rs by\n// `ASCRIBE_BLESS=1 cargo test -p ascribe-core --test names`. Don't edit it.\n\n/** `<ascribe-note>`: a note (packages/elements/CONTRACT.md §1). */\nexport const ELEMENT_NOTE = \"ascribe-note\";\n\n/** `<ascribe-steps>`: a procedure (packages/elements/CONTRACT.md §2). */\nexport const ELEMENT_STEPS = \"ascribe-steps\";\n\n/** `<ascribe-tabs>`: a variant group of several arms (packages/elements/CONTRACT.md §3). */\nexport const ELEMENT_TABS = \"ascribe-tabs\";\n\n/** `<ascribe-tab>`: one arm of `<ascribe-tabs>` (packages/elements/CONTRACT.md §3). */\nexport const ELEMENT_TAB = \"ascribe-tab\";\n\n/** `<ascribe-availability>`: a badge or filter (packages/elements/CONTRACT.md §4). */\nexport const ELEMENT_AVAILABILITY = \"ascribe-availability\";\n\n/**\n * `<ascribe-availability-target>`: one target of `<ascribe-availability>`\n * (packages/elements/CONTRACT.md §4).\n */\nexport const ELEMENT_AVAILABILITY_TARGET = \"ascribe-availability-target\";\n\n/**\n * On the visually hidden `(Available: ` and `)` around a table row's\n * `<ascribe-availability>` targets (packages/elements/CONTRACT.md §4).\n */\nexport const CLASS_AVAILABILITY_LEAD_IN = \"ascribe-availability-lead-in\";\n\n/**\n * On the line of a code block's caption that says what a tested example\n * was tested with; its lead-in, \"Tested with\", is the stylesheet's\n * (packages/elements/CONTRACT.md §9).\n */\nexport const CLASS_CODE_TESTED = \"ascribe-code-tested\";\n\n/**\n * `<ascribe-operation>`: an operation of a declared API, from `@operation`\n * (packages/elements/CONTRACT.md §11).\n */\nexport const ELEMENT_OPERATION = \"ascribe-operation\";\n\n/**\n * On the `(deprecated)` after a deprecated operation's link in\n * `<ascribe-operation>` (packages/elements/CONTRACT.md §11).\n */\nexport const CLASS_OPERATION_DEPRECATED = \"ascribe-operation-deprecated\";\n\n/** `<ascribe-group>`: a project widget's group (packages/elements/CONTRACT.md §6). */\nexport const ELEMENT_GROUP = \"ascribe-group\";\n\n/**\n * `<ascribe-for-agents>`: the visually hidden pointer to `llms.txt` at the top\n * of a page, for agents (packages/elements/CONTRACT.md §8).\n */\nexport const ELEMENT_FOR_AGENTS = \"ascribe-for-agents\";\n\n/** `<ascribe-attributes>`: the attribute marker (site-render contract §1). */\nexport const ELEMENT_ATTRIBUTES = \"ascribe-attributes\";\n\n/** The name an anchor comment starts with, `<!--ascribe-anchor` (site-render contract §7.3). */\nexport const COMMENT_ANCHOR = \"ascribe-anchor\";\n\n/** A block's source anchor (site-render contract §7.1). */\nexport const DATA_SOURCE = \"data-ascribe-source\";\n\n/** The includes a block came through (site-render contract §7.1). */\nexport const DATA_VIA = \"data-ascribe-via\";\n\n/** A glossary term's id, on its link (packages/elements/CONTRACT.md §7). */\nexport const DATA_TERM = \"data-ascribe-term\";\n\n/** The line a link out of a line leads to, on its link (packages/elements/CONTRACT.md §10). */\nexport const DATA_RELEASE_LINE = \"data-ascribe-release-line\";\n\n/** The element the report draws into. */\nexport const ID_REPORT = \"ascribe-review\";\n\n/** The `<script type=\"application/json\">` holding the report's data. */\nexport const ID_REPORT_DATA = \"ascribe-review-data\";\n\n/**\n * The page's light or dark scheme, `light` or `dark`, on the root\n * element (docs/content/astro/reference.md).\n */\nexport const DATA_SCHEME = \"data-ascribe-scheme\";\n\n/** On a marked page's root: what it shows, `changes`, `will`, or `was`. */\nexport const DATA_SHOW = \"data-ascribe-show\";\n\n/**\n * On a marked block: its change, `added`, `changed`, `removed`, `moved`,\n * or `moved-from`.\n */\nexport const DATA_CHANGE = \"data-ascribe-change\";\n\n/** On every element review adds to the page, so it can be taken away again. */\nexport const DATA_UI = \"data-ascribe-ui\";\n\n/** On a change's label: the change. */\nexport const DATA_LABEL = \"data-ascribe-label\";\n\n/** On a tab's hint: `added` or `changed`. */\nexport const DATA_HINT = \"data-ascribe-hint\";\n\n/** On a moved block: the id its \"moved from\" link points at. */\nexport const DATA_MOVE = \"data-ascribe-move\";\n\n/** On a changed block whose copy as it was follows it. */\nexport const DATA_HAS_WAS = \"data-ascribe-has-was\";\n\n/** On a copy of a block as it was, shown only with the page as it was. */\nexport const DATA_WAS_ONLY = \"data-ascribe-was-only\";\n\n/** On a changed, removed, or moved block: its source anchor as it was. */\nexport const DATA_WAS_SOURCE = \"data-ascribe-was-source\";\n\n/** On the elements that hold the threads' overlay. */\nexport const DATA_OVERLAY = \"data-ascribe-overlay\";\n\n/** On the style element the Astro dev toolbar adds for the marks. */\nexport const DATA_REVIEW = \"data-ascribe-review\";\n\n/** On the root of a page whose changes are marked. */\nexport const CLASS_MARKS = \"ascribe-marks\";\n\n/** A change's label. */\nexport const CLASS_LABEL = \"ascribe-label\";\n\n/** A label placed before its block rather than over it. */\nexport const CLASS_LABEL_BEFORE = \"ascribe-label-before\";\n\n/** Inserted text. */\nexport const CLASS_INS = \"ascribe-ins\";\n\n/** Deleted text. */\nexport const CLASS_DEL = \"ascribe-del\";\n\n/** A removed block. */\nexport const CLASS_REMOVED = \"ascribe-removed\";\n\n/** A removed block's content. */\nexport const CLASS_REMOVED_BODY = \"ascribe-removed-body\";\n\n/** The button that shows or collapses a removed block. */\nexport const CLASS_TOGGLE = \"ascribe-toggle\";\n\n/** On a removed block that's shown. */\nexport const CLASS_OPEN = \"ascribe-open\";\n\n/** What stands where a moved block was. */\nexport const CLASS_MOVED_FROM = \"ascribe-moved-from\";\n\n/** The note with a moved block's link. */\nexport const CLASS_MOVE_NOTE = \"ascribe-move-note\";\n\n/** A move note after its block, not inside it. */\nexport const CLASS_MOVE_AFTER = \"ascribe-move-after\";\n\n/** The link between a moved block and where it was. */\nexport const CLASS_MOVE_LINK = \"ascribe-move-link\";\n\n/** A moved block's copy as it was, where it was. */\nexport const CLASS_WAS_COPY = \"ascribe-was-copy\";\n\n/** A tab's hint that it holds changes. */\nexport const CLASS_HINT = \"ascribe-hint\";\n\n/** A block flashed after going to it. */\nexport const CLASS_FLASH = \"ascribe-flash\";\n\n/** The tip that shows a block's source on hover and focus. */\nexport const CLASS_WHERE = \"ascribe-where\";\n"]}
1
+ {"version":3,"file":"names.js","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,qDAAqD;AACrD,4EAA4E;AAE5E,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,yEAAyE;AACzE,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C,4FAA4F;AAC5F,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,uFAAuF;AACvF,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAEnD,sFAAsF;AACtF,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,6BAA6B,CAAC;AAEzE;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,8BAA8B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,cAAc,CAAC;AAEzC;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAErD;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,8BAA8B,CAAC;AAEzE,sFAAsF;AACtF,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,gGAAgG;AAChG,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C,2DAA2D;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD,qEAAqE;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C,+FAA+F;AAC/F,MAAM,CAAC,MAAM,iBAAiB,GAAG,2BAA2B,CAAC;AAE7D,yCAAyC;AACzC,MAAM,CAAC,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAE1C,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAEpD;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAEzC,uCAAuC;AACvC,MAAM,CAAC,MAAM,UAAU,GAAG,oBAAoB,CAAC;AAE/C,6CAA6C;AAC7C,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C,gEAAgE;AAChE,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAEnD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAErD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,eAAe,GAAG,yBAAyB,CAAC;AAEzD,sDAAsD;AACtD,MAAM,CAAC,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAEnD,qEAAqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD,sDAAsD;AACtD,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC;AAE3C,wBAAwB;AACxB,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC;AAE3C,2DAA2D;AAC3D,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD,qBAAqB;AACrB,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC;AAEvC,oBAAoB;AACpB,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC;AAEvC,uBAAuB;AACvB,MAAM,CAAC,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAE/C,iCAAiC;AACjC,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD,0DAA0D;AAC1D,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE7C,uCAAuC;AACvC,MAAM,CAAC,MAAM,UAAU,GAAG,cAAc,CAAC;AAEzC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AAErD,0CAA0C;AAC1C,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAEnD,kDAAkD;AAClD,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AAErD,uDAAuD;AACvD,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAEnD,oDAAoD;AACpD,MAAM,CAAC,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAEjD,0CAA0C;AAC1C,MAAM,CAAC,MAAM,UAAU,GAAG,cAAc,CAAC;AAEzC,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC;AAE3C,8DAA8D;AAC9D,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAC","sourcesContent":["// The names Ascribe puts on a page: elements, attributes, classes, and ids.\n// Generated from crates/ascribe-core/src/names.rs by\n// `ASCRIBE_BLESS=1 cargo test -p ascribe-core --test names`. Don't edit it.\n\n/** `<ascribe-note>`: a note (packages/elements/CONTRACT.md §1). */\nexport const ELEMENT_NOTE = \"ascribe-note\";\n\n/** `<ascribe-steps>`: a procedure (packages/elements/CONTRACT.md §2). */\nexport const ELEMENT_STEPS = \"ascribe-steps\";\n\n/** `<ascribe-tabs>`: a variant group of several arms (packages/elements/CONTRACT.md §3). */\nexport const ELEMENT_TABS = \"ascribe-tabs\";\n\n/** `<ascribe-tab>`: one arm of `<ascribe-tabs>` (packages/elements/CONTRACT.md §3). */\nexport const ELEMENT_TAB = \"ascribe-tab\";\n\n/**\n * On the paragraph that starts an `<ascribe-tab>` and holds its label as\n * text, for a reader of the HTML with no script (packages/elements/CONTRACT.md §3).\n */\nexport const CLASS_TAB_LABEL = \"ascribe-tab-label\";\n\n/** `<ascribe-availability>`: a badge or filter (packages/elements/CONTRACT.md §4). */\nexport const ELEMENT_AVAILABILITY = \"ascribe-availability\";\n\n/**\n * `<ascribe-availability-target>`: one target of `<ascribe-availability>`\n * (packages/elements/CONTRACT.md §4).\n */\nexport const ELEMENT_AVAILABILITY_TARGET = \"ascribe-availability-target\";\n\n/**\n * On the visually hidden `(Available: ` and `)` around a table row's\n * `<ascribe-availability>` targets (packages/elements/CONTRACT.md §4).\n */\nexport const CLASS_AVAILABILITY_LEAD_IN = \"ascribe-availability-lead-in\";\n\n/**\n * On the line of a code block's caption that says what a tested example\n * was tested with; its lead-in, \"Tested with\", is the stylesheet's\n * (packages/elements/CONTRACT.md §9).\n */\nexport const CLASS_CODE_TESTED = \"ascribe-code-tested\";\n\n/**\n * On the button the element library's script adds after a code block's\n * `<pre>`, which copies its code; its words are the stylesheet's\n * (packages/elements/CONTRACT.md §12).\n */\nexport const CLASS_COPY = \"ascribe-copy\";\n\n/**\n * On a copy button for a moment after a click: `copied`, or `failed`\n * when the clipboard refused (packages/elements/CONTRACT.md §12).\n */\nexport const DATA_COPY = \"data-ascribe-copy\";\n\n/**\n * The visually hidden live region that says a copy button copied\n * (packages/elements/CONTRACT.md §12).\n */\nexport const CLASS_COPY_STATUS = \"ascribe-copy-status\";\n\n/**\n * `<ascribe-operation>`: an operation of a declared API, from `@operation`\n * (packages/elements/CONTRACT.md §11).\n */\nexport const ELEMENT_OPERATION = \"ascribe-operation\";\n\n/**\n * On the `(deprecated)` after a deprecated operation's link in\n * `<ascribe-operation>` (packages/elements/CONTRACT.md §11).\n */\nexport const CLASS_OPERATION_DEPRECATED = \"ascribe-operation-deprecated\";\n\n/** `<ascribe-group>`: a project widget's group (packages/elements/CONTRACT.md §6). */\nexport const ELEMENT_GROUP = \"ascribe-group\";\n\n/**\n * `<ascribe-for-agents>`: the visually hidden pointer to `llms.txt` at the top\n * of a page, for agents (packages/elements/CONTRACT.md §8).\n */\nexport const ELEMENT_FOR_AGENTS = \"ascribe-for-agents\";\n\n/** `<ascribe-attributes>`: the attribute marker (site-render contract §1). */\nexport const ELEMENT_ATTRIBUTES = \"ascribe-attributes\";\n\n/** The name an anchor comment starts with, `<!--ascribe-anchor` (site-render contract §7.3). */\nexport const COMMENT_ANCHOR = \"ascribe-anchor\";\n\n/** A block's source anchor (site-render contract §7.1). */\nexport const DATA_SOURCE = \"data-ascribe-source\";\n\n/** The includes a block came through (site-render contract §7.1). */\nexport const DATA_VIA = \"data-ascribe-via\";\n\n/** A glossary term's id, on its link (packages/elements/CONTRACT.md §7). */\nexport const DATA_TERM = \"data-ascribe-term\";\n\n/** The line a link out of a line leads to, on its link (packages/elements/CONTRACT.md §10). */\nexport const DATA_RELEASE_LINE = \"data-ascribe-release-line\";\n\n/** The element the report draws into. */\nexport const ID_REPORT = \"ascribe-review\";\n\n/** The `<script type=\"application/json\">` holding the report's data. */\nexport const ID_REPORT_DATA = \"ascribe-review-data\";\n\n/**\n * The page's light or dark scheme, `light` or `dark`, on the root\n * element (docs/content/astro/reference.md).\n */\nexport const DATA_SCHEME = \"data-ascribe-scheme\";\n\n/** On a marked page's root: what it shows, `changes`, `will`, or `was`. */\nexport const DATA_SHOW = \"data-ascribe-show\";\n\n/**\n * On a marked block: its change, `added`, `changed`, `removed`, `moved`,\n * or `moved-from`.\n */\nexport const DATA_CHANGE = \"data-ascribe-change\";\n\n/** On every element review adds to the page, so it can be taken away again. */\nexport const DATA_UI = \"data-ascribe-ui\";\n\n/** On a change's label: the change. */\nexport const DATA_LABEL = \"data-ascribe-label\";\n\n/** On a tab's hint: `added` or `changed`. */\nexport const DATA_HINT = \"data-ascribe-hint\";\n\n/** On a moved block: the id its \"moved from\" link points at. */\nexport const DATA_MOVE = \"data-ascribe-move\";\n\n/** On a changed block whose copy as it was follows it. */\nexport const DATA_HAS_WAS = \"data-ascribe-has-was\";\n\n/** On a copy of a block as it was, shown only with the page as it was. */\nexport const DATA_WAS_ONLY = \"data-ascribe-was-only\";\n\n/** On a changed, removed, or moved block: its source anchor as it was. */\nexport const DATA_WAS_SOURCE = \"data-ascribe-was-source\";\n\n/** On the elements that hold the threads' overlay. */\nexport const DATA_OVERLAY = \"data-ascribe-overlay\";\n\n/** On the style element the Astro dev toolbar adds for the marks. */\nexport const DATA_REVIEW = \"data-ascribe-review\";\n\n/** On the root of a page whose changes are marked. */\nexport const CLASS_MARKS = \"ascribe-marks\";\n\n/** A change's label. */\nexport const CLASS_LABEL = \"ascribe-label\";\n\n/** A label placed before its block rather than over it. */\nexport const CLASS_LABEL_BEFORE = \"ascribe-label-before\";\n\n/** Inserted text. */\nexport const CLASS_INS = \"ascribe-ins\";\n\n/** Deleted text. */\nexport const CLASS_DEL = \"ascribe-del\";\n\n/** A removed block. */\nexport const CLASS_REMOVED = \"ascribe-removed\";\n\n/** A removed block's content. */\nexport const CLASS_REMOVED_BODY = \"ascribe-removed-body\";\n\n/** The button that shows or collapses a removed block. */\nexport const CLASS_TOGGLE = \"ascribe-toggle\";\n\n/** On a removed block that's shown. */\nexport const CLASS_OPEN = \"ascribe-open\";\n\n/** What stands where a moved block was. */\nexport const CLASS_MOVED_FROM = \"ascribe-moved-from\";\n\n/** The note with a moved block's link. */\nexport const CLASS_MOVE_NOTE = \"ascribe-move-note\";\n\n/** A move note after its block, not inside it. */\nexport const CLASS_MOVE_AFTER = \"ascribe-move-after\";\n\n/** The link between a moved block and where it was. */\nexport const CLASS_MOVE_LINK = \"ascribe-move-link\";\n\n/** A moved block's copy as it was, where it was. */\nexport const CLASS_WAS_COPY = \"ascribe-was-copy\";\n\n/** A tab's hint that it holds changes. */\nexport const CLASS_HINT = \"ascribe-hint\";\n\n/** A block flashed after going to it. */\nexport const CLASS_FLASH = \"ascribe-flash\";\n\n/** The tip that shows a block's source on hover and focus. */\nexport const CLASS_WHERE = \"ascribe-where\";\n"]}
package/dist/tabs.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // <ascribe-tabs> and <ascribe-tab> (CONTRACT.md §3): the library's only
2
2
  // element with behavior. Without this script every tab shows with its label
3
- // (CSS generated content); with it, the group follows the WAI-ARIA tabs
4
- // pattern.
5
- import { ELEMENT_TAB } from "./names.js";
3
+ // (its first paragraph); with it, the group follows the WAI-ARIA tabs
4
+ // pattern, and the paragraph is out of sight and hidden from screen readers,
5
+ // which hear the label from the tab's button.
6
+ import { CLASS_TAB_LABEL, ELEMENT_TAB } from "./names.js";
6
7
  const STORAGE_PREFIX = "ascribe-tabs:";
7
8
  /** Choices made this page load, for readers whose storage is unavailable. */
8
9
  const pageChoices = new Map();
@@ -35,6 +36,11 @@ function writeChoice(dimension, value) {
35
36
  function valuesOf(tab) {
36
37
  return (tab.getAttribute("value") ?? "").split(/\s+/).filter(Boolean);
37
38
  }
39
+ /** A tab's label paragraph, its label as text, when it has one. */
40
+ function labelOf(tab) {
41
+ const first = tab.firstElementChild;
42
+ return first?.classList.contains(CLASS_TAB_LABEL) ? first : undefined;
43
+ }
38
44
  /** Select `value` in every group on the page that switches on `dimension`, but `except`. */
39
45
  let followChoice;
40
46
  /**
@@ -100,6 +106,7 @@ export class AscribeTabs extends HTMLElement {
100
106
  tablist.append(button);
101
107
  tab.setAttribute("role", "tabpanel");
102
108
  tab.setAttribute("aria-labelledby", button.id);
109
+ labelOf(tab)?.setAttribute("aria-hidden", "true");
103
110
  return button;
104
111
  });
105
112
  this.prepend(tablist);
@@ -123,6 +130,7 @@ export class AscribeTabs extends HTMLElement {
123
130
  tab.removeAttribute("role");
124
131
  tab.removeAttribute("aria-labelledby");
125
132
  tab.removeAttribute("hidden");
133
+ labelOf(tab)?.removeAttribute("aria-hidden");
126
134
  }
127
135
  this.#tablist = null;
128
136
  this.#tabs = [];
package/dist/tabs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","sourceRoot":"","sources":["../src/tabs.ts"],"names":[],"mappings":"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","sourcesContent":["// <ascribe-tabs> and <ascribe-tab> (CONTRACT.md §3): the library's only\n// element with behavior. Without this script every tab shows with its label\n// (CSS generated content); with it, the group follows the WAI-ARIA tabs\n// pattern.\n\nimport { ELEMENT_TAB } from \"./names.js\";\n\nconst STORAGE_PREFIX = \"ascribe-tabs:\";\n\n/** Choices made this page load, for readers whose storage is unavailable. */\nconst pageChoices = new Map<string, string>();\n\n/** Every connected, initialized group with a `sync` attribute. */\nconst syncedGroups = new Set<AscribeTabs>();\n\nlet idCounter = 0;\n\nfunction readChoice(dimension: string): string | null {\n // This page's choices are always at least as fresh as storage, which a\n // failed write leaves behind.\n const chosen = pageChoices.get(dimension);\n if (chosen !== undefined) return chosen;\n try {\n return localStorage.getItem(STORAGE_PREFIX + dimension);\n } catch {\n // Storage can be blocked or unavailable.\n return null;\n }\n}\n\nfunction writeChoice(dimension: string, value: string): void {\n pageChoices.set(dimension, value);\n try {\n localStorage.setItem(STORAGE_PREFIX + dimension, value);\n } catch {\n // The choice lasts for the page only.\n }\n}\n\nfunction valuesOf(tab: Element): string[] {\n return (tab.getAttribute(\"value\") ?? \"\").split(/\\s+/).filter(Boolean);\n}\n\n/** Select `value` in every group on the page that switches on `dimension`, but `except`. */\nlet followChoice: (dimension: string, value: string, except?: AscribeTabs) => void;\n\n/**\n * Choose `value` for `dimension`, as a reader selecting a tab with that value\n * does: remember it across pages, and select it in every group on this page\n * that switches on the dimension. A group with no tab for it keeps its\n * selection. The value isn't checked.\n */\nexport function choose(dimension: string, value: string): void {\n writeChoice(dimension, value);\n followChoice(dimension, value);\n}\n\n/** A tab panel: one arm of a `@variant` group. Styled by CSS; no behavior. */\nexport class AscribeTab extends HTMLElement {}\n\nexport class AscribeTabs extends HTMLElement {\n #tablist: HTMLElement | null = null;\n #tabs: HTMLElement[] = [];\n #buttons: HTMLButtonElement[] = [];\n #selected = 0;\n #ready = false;\n\n static {\n followChoice = (dimension, value, except) => {\n for (const group of syncedGroups) {\n if (group !== except && group.getAttribute(\"sync\") === dimension) group.#follow(value);\n }\n };\n }\n\n connectedCallback(): void {\n // A parser-created element is connected before its children are parsed.\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", () => this.#setup(), { once: true });\n } else {\n this.#setup();\n }\n }\n\n disconnectedCallback(): void {\n this.#teardown();\n }\n\n #setup(): void {\n if (!this.isConnected || this.#ready) return;\n this.#tabs = Array.from(this.children).filter(\n (child): child is HTMLElement => child.localName === ELEMENT_TAB,\n );\n if (this.#tabs.length === 0) return;\n this.#ready = true;\n\n const tablist = document.createElement(\"div\");\n tablist.setAttribute(\"role\", \"tablist\");\n this.#tablist = tablist;\n\n this.#buttons = this.#tabs.map((tab) => {\n if (!tab.id) tab.id = `ascribe-tab-${++idCounter}`;\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.id = `${tab.id}-button`;\n button.setAttribute(\"role\", \"tab\");\n button.setAttribute(\"aria-controls\", tab.id);\n button.textContent = tab.getAttribute(\"label\") ?? \"\";\n button.addEventListener(\"click\", () => this.#choose(this.#buttons.indexOf(button)));\n button.addEventListener(\"keydown\", (event) => this.#onKeydown(event));\n tablist.append(button);\n tab.setAttribute(\"role\", \"tabpanel\");\n tab.setAttribute(\"aria-labelledby\", button.id);\n return button;\n });\n this.prepend(tablist);\n\n const sync = this.getAttribute(\"sync\");\n let initial = 0;\n if (sync !== null) {\n syncedGroups.add(this);\n const remembered = readChoice(sync);\n const match = remembered === null ? -1 : this.#indexOfValue(remembered);\n if (match >= 0) initial = match;\n }\n this.#select(initial);\n }\n\n #teardown(): void {\n if (!this.#ready) return;\n syncedGroups.delete(this);\n this.#tablist?.remove();\n for (const tab of this.#tabs) {\n tab.removeAttribute(\"role\");\n tab.removeAttribute(\"aria-labelledby\");\n tab.removeAttribute(\"hidden\");\n }\n this.#tablist = null;\n this.#tabs = [];\n this.#buttons = [];\n this.#ready = false;\n }\n\n #indexOfValue(value: string): number {\n return this.#tabs.findIndex((tab) => valuesOf(tab).includes(value));\n }\n\n /** Select a tab without recording or syncing the choice. */\n #select(index: number): void {\n this.#selected = index;\n this.#tabs.forEach((tab, i) => {\n tab.toggleAttribute(\"hidden\", i !== index);\n });\n this.#buttons.forEach((button, i) => {\n button.setAttribute(\"aria-selected\", String(i === index));\n button.tabIndex = i === index ? 0 : -1;\n });\n }\n\n /** The reader picked a tab: select it and, for a synced group, tell the rest. */\n #choose(index: number): void {\n this.#select(index);\n const sync = this.getAttribute(\"sync\");\n const tab = this.#tabs[index];\n const choice = tab ? valuesOf(tab)[0] : undefined;\n if (sync === null || choice === undefined) return;\n writeChoice(sync, choice);\n followChoice(sync, choice, this);\n }\n\n /** Another group with the same `sync` made a choice; keep our selection if we have no match. */\n #follow(value: string): void {\n const index = this.#indexOfValue(value);\n if (index >= 0) this.#select(index);\n }\n\n #onKeydown(event: KeyboardEvent): void {\n const last = this.#tabs.length - 1;\n let next: number;\n switch (event.key) {\n case \"ArrowRight\":\n next = this.#selected === last ? 0 : this.#selected + 1;\n break;\n case \"ArrowLeft\":\n next = this.#selected === 0 ? last : this.#selected - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#choose(next);\n this.#buttons[next]?.focus();\n }\n}\n"]}
1
+ {"version":3,"file":"tabs.js","sourceRoot":"","sources":["../src/tabs.ts"],"names":[],"mappings":"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","sourcesContent":["// <ascribe-tabs> and <ascribe-tab> (CONTRACT.md §3): the library's only\n// element with behavior. Without this script every tab shows with its label\n// (its first paragraph); with it, the group follows the WAI-ARIA tabs\n// pattern, and the paragraph is out of sight and hidden from screen readers,\n// which hear the label from the tab's button.\n\nimport { CLASS_TAB_LABEL, ELEMENT_TAB } from \"./names.js\";\n\nconst STORAGE_PREFIX = \"ascribe-tabs:\";\n\n/** Choices made this page load, for readers whose storage is unavailable. */\nconst pageChoices = new Map<string, string>();\n\n/** Every connected, initialized group with a `sync` attribute. */\nconst syncedGroups = new Set<AscribeTabs>();\n\nlet idCounter = 0;\n\nfunction readChoice(dimension: string): string | null {\n // This page's choices are always at least as fresh as storage, which a\n // failed write leaves behind.\n const chosen = pageChoices.get(dimension);\n if (chosen !== undefined) return chosen;\n try {\n return localStorage.getItem(STORAGE_PREFIX + dimension);\n } catch {\n // Storage can be blocked or unavailable.\n return null;\n }\n}\n\nfunction writeChoice(dimension: string, value: string): void {\n pageChoices.set(dimension, value);\n try {\n localStorage.setItem(STORAGE_PREFIX + dimension, value);\n } catch {\n // The choice lasts for the page only.\n }\n}\n\nfunction valuesOf(tab: Element): string[] {\n return (tab.getAttribute(\"value\") ?? \"\").split(/\\s+/).filter(Boolean);\n}\n\n/** A tab's label paragraph, its label as text, when it has one. */\nfunction labelOf(tab: Element): Element | undefined {\n const first = tab.firstElementChild;\n return first?.classList.contains(CLASS_TAB_LABEL) ? first : undefined;\n}\n\n/** Select `value` in every group on the page that switches on `dimension`, but `except`. */\nlet followChoice: (dimension: string, value: string, except?: AscribeTabs) => void;\n\n/**\n * Choose `value` for `dimension`, as a reader selecting a tab with that value\n * does: remember it across pages, and select it in every group on this page\n * that switches on the dimension. A group with no tab for it keeps its\n * selection. The value isn't checked.\n */\nexport function choose(dimension: string, value: string): void {\n writeChoice(dimension, value);\n followChoice(dimension, value);\n}\n\n/** A tab panel: one arm of a `@variant` group. Styled by CSS; no behavior. */\nexport class AscribeTab extends HTMLElement {}\n\nexport class AscribeTabs extends HTMLElement {\n #tablist: HTMLElement | null = null;\n #tabs: HTMLElement[] = [];\n #buttons: HTMLButtonElement[] = [];\n #selected = 0;\n #ready = false;\n\n static {\n followChoice = (dimension, value, except) => {\n for (const group of syncedGroups) {\n if (group !== except && group.getAttribute(\"sync\") === dimension) group.#follow(value);\n }\n };\n }\n\n connectedCallback(): void {\n // A parser-created element is connected before its children are parsed.\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", () => this.#setup(), { once: true });\n } else {\n this.#setup();\n }\n }\n\n disconnectedCallback(): void {\n this.#teardown();\n }\n\n #setup(): void {\n if (!this.isConnected || this.#ready) return;\n this.#tabs = Array.from(this.children).filter(\n (child): child is HTMLElement => child.localName === ELEMENT_TAB,\n );\n if (this.#tabs.length === 0) return;\n this.#ready = true;\n\n const tablist = document.createElement(\"div\");\n tablist.setAttribute(\"role\", \"tablist\");\n this.#tablist = tablist;\n\n this.#buttons = this.#tabs.map((tab) => {\n if (!tab.id) tab.id = `ascribe-tab-${++idCounter}`;\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.id = `${tab.id}-button`;\n button.setAttribute(\"role\", \"tab\");\n button.setAttribute(\"aria-controls\", tab.id);\n button.textContent = tab.getAttribute(\"label\") ?? \"\";\n button.addEventListener(\"click\", () => this.#choose(this.#buttons.indexOf(button)));\n button.addEventListener(\"keydown\", (event) => this.#onKeydown(event));\n tablist.append(button);\n tab.setAttribute(\"role\", \"tabpanel\");\n tab.setAttribute(\"aria-labelledby\", button.id);\n labelOf(tab)?.setAttribute(\"aria-hidden\", \"true\");\n return button;\n });\n this.prepend(tablist);\n\n const sync = this.getAttribute(\"sync\");\n let initial = 0;\n if (sync !== null) {\n syncedGroups.add(this);\n const remembered = readChoice(sync);\n const match = remembered === null ? -1 : this.#indexOfValue(remembered);\n if (match >= 0) initial = match;\n }\n this.#select(initial);\n }\n\n #teardown(): void {\n if (!this.#ready) return;\n syncedGroups.delete(this);\n this.#tablist?.remove();\n for (const tab of this.#tabs) {\n tab.removeAttribute(\"role\");\n tab.removeAttribute(\"aria-labelledby\");\n tab.removeAttribute(\"hidden\");\n labelOf(tab)?.removeAttribute(\"aria-hidden\");\n }\n this.#tablist = null;\n this.#tabs = [];\n this.#buttons = [];\n this.#ready = false;\n }\n\n #indexOfValue(value: string): number {\n return this.#tabs.findIndex((tab) => valuesOf(tab).includes(value));\n }\n\n /** Select a tab without recording or syncing the choice. */\n #select(index: number): void {\n this.#selected = index;\n this.#tabs.forEach((tab, i) => {\n tab.toggleAttribute(\"hidden\", i !== index);\n });\n this.#buttons.forEach((button, i) => {\n button.setAttribute(\"aria-selected\", String(i === index));\n button.tabIndex = i === index ? 0 : -1;\n });\n }\n\n /** The reader picked a tab: select it and, for a synced group, tell the rest. */\n #choose(index: number): void {\n this.#select(index);\n const sync = this.getAttribute(\"sync\");\n const tab = this.#tabs[index];\n const choice = tab ? valuesOf(tab)[0] : undefined;\n if (sync === null || choice === undefined) return;\n writeChoice(sync, choice);\n followChoice(sync, choice, this);\n }\n\n /** Another group with the same `sync` made a choice; keep our selection if we have no match. */\n #follow(value: string): void {\n const index = this.#indexOfValue(value);\n if (index >= 0) this.#select(index);\n }\n\n #onKeydown(event: KeyboardEvent): void {\n const last = this.#tabs.length - 1;\n let next: number;\n switch (event.key) {\n case \"ArrowRight\":\n next = this.#selected === last ? 0 : this.#selected + 1;\n break;\n case \"ArrowLeft\":\n next = this.#selected === 0 ? last : this.#selected - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#choose(next);\n this.#buttons[next]?.focus();\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascribed/elements",
3
- "version": "0.4.4",
3
+ "version": "0.4.5-next.34",
4
4
  "description": "A toolchain for documentation written as code. This package is the web components its site output uses.",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -44,6 +44,7 @@
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  },
47
+ "gitHead": "570d8f155ffbc71857ce05a47a70840f1bbab67e",
47
48
  "scripts": {
48
49
  "build": "tsc -p tsconfig.build.json",
49
50
  "typecheck": "tsc --noEmit",