@primitiv-ui/web 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,13 @@
1
1
  import { deriveId, tabsTabStop, } from "@primitiv-ui/core";
2
+ import { defineElements, ElementBase, reportOnce, write } from "../internal/element.js";
3
+ import { PrimitivRootElement } from "../internal/RootElement.js";
2
4
  import { TabsController } from "./TabsController.js";
3
- /**
4
- * The base class every element extends. It falls back to an empty class where
5
- * the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
6
- * Astro) can import this module without `HTMLElement` throwing at load time
7
- * (RFC 0033 D21).
8
- */
9
- const ElementBase = globalThis.HTMLElement ?? class {
10
- };
11
5
  // Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineTabs registry test instead.
12
6
  const ROOT = "primitiv-tabs";
13
7
  const LIST = "primitiv-tabs-list";
14
8
  const TRIGGER = "primitiv-tabs-trigger";
15
9
  const CONTENT = "primitiv-tabs-content";
16
10
  // Stryker restore StringLiteral
17
- let generatedIds = 0;
18
- /** Triggers already reported as misused, so each is reported once, not per reconcile. */
19
- const reported = new WeakSet();
20
11
  /**
21
12
  * An interactive part decorates exactly one element child and needs a `value`.
22
13
  * Anything else is reported once and left undecorated rather than thrown: a
@@ -27,37 +18,17 @@ function decoratable(trigger) {
27
18
  if (trigger.childElementCount === 1 && value !== null) {
28
19
  return { tab: trigger.firstElementChild, value };
29
20
  }
30
- if (!reported.has(trigger)) {
31
- reported.add(trigger);
32
- console.error(`<${TRIGGER}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`, trigger);
33
- }
21
+ reportOnce(trigger, `<${TRIGGER}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`);
34
22
  return null;
35
23
  }
36
- /**
37
- * Write an attribute only when it would change: `null` removes it. The elements
38
- * write only the attributes in their contract (RFC 0033 §5.3), and comparing
39
- * first means a value a framework or the server already rendered is never
40
- * rewritten (D20).
41
- */
42
- function write(element, name, value) {
43
- if (element.getAttribute(name) === value)
44
- return;
45
- if (value === null)
46
- element.removeAttribute(name);
47
- else
48
- element.setAttribute(name, value);
49
- }
50
24
  /**
51
25
  * `<primitiv-tabs>` — the state owner of a WAI-ARIA tab set.
52
26
  *
53
- * The root finds its own parts, scoped with `closest()` so a tab set nested in a
54
- * panel keeps its own, and reconciles their ARIA from its reflected state. It
55
- * never caches the parts, so markup that arrives later is picked up on the next
56
- * reconcile. One `MutationObserver` is the only trigger after connect: it sees
57
- * parts arriving and leaving, and every attribute the root reads, including the
58
- * root's own `value` and `orientation`, and it already delivers a task's changes
59
- * as one callback, so no further batching is needed. `activation-mode` is read
60
- * live at each key press, so a change to it needs no reconcile.
27
+ * The root finds its own parts, so a tab set nested in a panel keeps its own,
28
+ * and reconciles their ARIA from its reflected state whenever a part arrives,
29
+ * leaves, or changes an attribute the root reads (the lifecycle is
30
+ * `PrimitivRootElement`'s). `activation-mode` is read live at each key press,
31
+ * so a change to it needs no reconcile.
61
32
  *
62
33
  * @customElement primitiv-tabs
63
34
  * @summary The state owner of a WAI-ARIA tab set.
@@ -66,37 +37,12 @@ function write(element, name, value) {
66
37
  * @attr {"automatic" | "manual"} activation-mode - `automatic` activates a tab as focus arrives; `manual` waits for Enter or Space. Defaults to `automatic`.
67
38
  * @fires {CustomEvent<{ value: string; index: number }>} primitiv-tabs-change - Bubbles, cancelable, fired before a user-driven change is applied. Call `preventDefault()` to veto it.
68
39
  */
69
- export class PrimitivTabsElement extends ElementBase {
70
- #generatedId;
40
+ export class PrimitivTabsElement extends PrimitivRootElement {
71
41
  /** The value of the panel last shown, so a change fires primitiv-tabs-show. */
72
42
  #shown;
73
43
  #rendered = false;
74
- #controller = new TabsController(this, this.#host());
75
- /**
76
- * Watches the subtree for parts arriving, leaving, or changing the attributes
77
- * the root reads. The root's own writes (role, aria-*, ids, data-*) are not in
78
- * the filter, so reconciling never re-triggers itself.
79
- */
80
- #observer = new MutationObserver(() => this.#reconcile());
81
- connectedCallback() {
82
- this.#controller.connect();
83
- this.#observer.observe(this, {
84
- subtree: true,
85
- childList: true,
86
- attributes: true,
87
- attributeFilter: ["value", "disabled", "label", "orientation"],
88
- });
89
- this.#reconcile();
90
- }
91
- /**
92
- * Leaving the document stops all listening. A move (an `hx-preserve`, a
93
- * framework reparenting) is a disconnect followed by a connect, so handlers
94
- * never stack.
95
- */
96
- disconnectedCallback() {
97
- this.#controller.disconnect();
98
- this.#observer.disconnect();
99
- }
44
+ controller = new TabsController(this, this.#host());
45
+ reconcileOn = ["value", "disabled", "label", "orientation"];
100
46
  /** The active tab's value, reflected as the `value` attribute. */
101
47
  get value() {
102
48
  return this.getAttribute("value") ?? undefined;
@@ -104,27 +50,17 @@ export class PrimitivTabsElement extends ElementBase {
104
50
  set value(next) {
105
51
  this.setAttribute("value", next);
106
52
  }
107
- /**
108
- * The controller's view of this element. State lives in reflected attributes;
109
- * an activation is announced first, as a cancelable event, so a listener can
110
- * veto it — the elements' equivalent of React's controlled mode (D7).
111
- */
53
+ /** The controller's view of this element: state lives in reflected attributes. */
112
54
  #host() {
113
55
  return {
114
56
  state: () => ({
115
57
  active: this.value,
116
58
  orientation: this.#orientation(),
117
- // Stryker disable next-line StringLiteral: equivalent — core tests only for "rtl", so any other string reads as left-to-right.
118
- dir: this.matches(":dir(rtl)") ? "rtl" : "ltr",
59
+ dir: this.direction(),
119
60
  activationMode: this.#activationMode(),
120
61
  }),
121
62
  activate: (value, index) => {
122
- const change = new CustomEvent("primitiv-tabs-change", {
123
- bubbles: true,
124
- cancelable: true,
125
- detail: { value, index },
126
- });
127
- if (this.dispatchEvent(change))
63
+ if (this.announce("primitiv-tabs-change", { value, index }))
128
64
  this.value = value;
129
65
  },
130
66
  };
@@ -136,20 +72,12 @@ export class PrimitivTabsElement extends ElementBase {
136
72
  #orientation() {
137
73
  return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
138
74
  }
139
- /** Parts of this tab set only, never those of a nested one. */
140
- #parts(selector) {
141
- return [...this.querySelectorAll(selector)].filter((part) => part.closest(ROOT) === this);
142
- }
143
- #rootId() {
144
- // Stryker disable next-line UpdateOperator: equivalent — counting down yields ids just as unique.
145
- return this.id || (this.#generatedId ??= `${ROOT}-${++generatedIds}`);
146
- }
147
- #reconcile() {
75
+ reconcile() {
148
76
  const orientation = this.#orientation();
149
77
  const active = this.value;
150
- const rootId = this.#rootId();
78
+ const rootId = this.rootId();
151
79
  write(this, "data-orientation", orientation);
152
- for (const list of this.#parts(LIST)) {
80
+ for (const list of this.parts(LIST)) {
153
81
  write(list, "role", "tablist");
154
82
  write(list, "aria-orientation", orientation);
155
83
  write(list, "data-orientation", orientation);
@@ -158,19 +86,19 @@ export class PrimitivTabsElement extends ElementBase {
158
86
  write(list, "aria-label", label);
159
87
  }
160
88
  const panels = new Map();
161
- for (const panel of this.#parts(CONTENT)) {
89
+ for (const panel of this.parts(CONTENT)) {
162
90
  const value = panel.getAttribute("value");
163
91
  if (value !== null)
164
92
  panels.set(value, panel);
165
93
  }
166
- const triggers = this.#parts(TRIGGER).flatMap((trigger) => {
94
+ const triggers = this.parts(TRIGGER).flatMap((trigger) => {
167
95
  const part = decoratable(trigger);
168
96
  return part ? [{ trigger, ...part }] : [];
169
97
  });
170
98
  const stop = tabsTabStop({ values: triggers.map((t) => t.value), active });
171
99
  // Replace the registration wholesale: a removed part, or a trigger whose child
172
100
  // a framework re-rendered, simply is not registered again.
173
- this.#controller.setTriggers(triggers.map(({ trigger, tab, value }) => ({
101
+ this.controller.setTriggers(triggers.map(({ trigger, tab, value }) => ({
174
102
  value,
175
103
  element: tab,
176
104
  disabled: trigger.hasAttribute("disabled"),
@@ -250,16 +178,10 @@ export class PrimitivTabsContentElement extends ElementBase {
250
178
  * render).
251
179
  */
252
180
  export function defineTabs(registry = globalThis.customElements) {
253
- if (!registry)
254
- return;
255
- const elements = [
181
+ defineElements(registry, [
256
182
  [ROOT, PrimitivTabsElement],
257
183
  [LIST, PrimitivTabsListElement],
258
184
  [TRIGGER, PrimitivTabsTriggerElement],
259
185
  [CONTENT, PrimitivTabsContentElement],
260
- ];
261
- for (const [name, element] of elements) {
262
- if (!registry.get(name))
263
- registry.define(name, element);
264
- }
186
+ ]);
265
187
  }
package/dist/tags.d.ts CHANGED
@@ -12,9 +12,15 @@
12
12
  * It is a separate, npm-only entry because it augments a global type, which
13
13
  * JSR does not allow a published module to do (RFC 0033 §5.6.4).
14
14
  */
15
+ import type { PrimitivAccordionContentElement, PrimitivAccordionElement, PrimitivAccordionItemElement, PrimitivAccordionTriggerElement, PrimitivAccordionTriggerIconElement } from "./accordion/elements.js";
15
16
  import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "./tabs/elements.js";
16
17
  declare global {
17
18
  interface HTMLElementTagNameMap {
19
+ "primitiv-accordion": PrimitivAccordionElement;
20
+ "primitiv-accordion-item": PrimitivAccordionItemElement;
21
+ "primitiv-accordion-trigger": PrimitivAccordionTriggerElement;
22
+ "primitiv-accordion-content": PrimitivAccordionContentElement;
23
+ "primitiv-accordion-trigger-icon": PrimitivAccordionTriggerIconElement;
18
24
  "primitiv-tabs": PrimitivTabsElement;
19
25
  "primitiv-tabs-list": PrimitivTabsListElement;
20
26
  "primitiv-tabs-trigger": PrimitivTabsTriggerElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/web",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Primitiv's headless components as standard custom elements: light DOM, no framework, no runtime dependencies. Works in plain HTML, server-rendered stacks, React, Vue, Angular and anything else that renders HTML.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,10 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
+ "./accordion": {
14
+ "types": "./dist/accordion/index.d.ts",
15
+ "default": "./dist/accordion/index.js"
16
+ },
13
17
  "./tabs": {
14
18
  "types": "./dist/tabs/index.d.ts",
15
19
  "default": "./dist/tabs/index.js"
@@ -18,6 +22,10 @@
18
22
  "types": "./dist/define/index.d.ts",
19
23
  "default": "./dist/define/index.js"
20
24
  },
25
+ "./define/accordion": {
26
+ "types": "./dist/define/accordion.d.ts",
27
+ "default": "./dist/define/accordion.js"
28
+ },
21
29
  "./define/tabs": {
22
30
  "types": "./dist/define/tabs.d.ts",
23
31
  "default": "./dist/define/tabs.js"
@@ -52,7 +60,7 @@
52
60
  "directory": "packages/web"
53
61
  },
54
62
  "dependencies": {
55
- "@primitiv-ui/core": "0.1.43"
63
+ "@primitiv-ui/core": "0.1.44"
56
64
  },
57
65
  "devDependencies": {
58
66
  "@custom-elements-manifest/analyzer": "0.11.0",