elements-kit 0.0.20 → 0.2.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.
Files changed (77) hide show
  1. package/dist/attributes.d.mts +1 -1
  2. package/dist/benchmark.CX_oY03V-CsUg-gW0.mjs +5176 -0
  3. package/dist/custom-elements.d.mts +1 -1
  4. package/dist/for.d.mts +1 -1
  5. package/dist/{infer-DuFY-y2b.d.mts → infer-BUUWMoG9.d.mts} +3 -3
  6. package/dist/integrations/react.d.mts +1 -1
  7. package/dist/jsx-runtime/index.d.mts +1 -1
  8. package/dist/signals/index.d.mts +1 -1
  9. package/dist/slot.d.mts +1 -1
  10. package/dist/{test.BmQO5GaM-BeO5pvCo.mjs → test.BmQO5GaM-ZC2MPXQb.mjs} +6 -5153
  11. package/dist/utilities/active-element.d.mts +1 -1
  12. package/dist/utilities/active-element.test.mjs +2 -1
  13. package/dist/utilities/async.d.mts +1 -1
  14. package/dist/utilities/async.test.mjs +2 -1
  15. package/dist/utilities/context.d.mts +78 -0
  16. package/dist/utilities/context.mjs +115 -0
  17. package/dist/utilities/context.test.d.mts +1 -0
  18. package/dist/utilities/context.test.mjs +171 -0
  19. package/dist/utilities/debounced.d.mts +1 -1
  20. package/dist/utilities/debounced.test.mjs +2 -1
  21. package/dist/utilities/dom-lifecycle.bench.d.mts +1 -0
  22. package/dist/utilities/dom-lifecycle.bench.mjs +90 -0
  23. package/dist/utilities/dom-lifecycle.d.mts +88 -0
  24. package/dist/utilities/dom-lifecycle.mjs +113 -0
  25. package/dist/utilities/dom-lifecycle.test.d.mts +1 -0
  26. package/dist/utilities/dom-lifecycle.test.mjs +227 -0
  27. package/dist/utilities/element-rect.d.mts +1 -1
  28. package/dist/utilities/element-rect.test.mjs +2 -1
  29. package/dist/utilities/element-scroll.d.mts +1 -1
  30. package/dist/utilities/element-scroll.test.mjs +2 -1
  31. package/dist/utilities/event-driven.d.mts +1 -1
  32. package/dist/utilities/event-listener.d.mts +1 -1
  33. package/dist/utilities/event-listener.test.mjs +2 -1
  34. package/dist/utilities/focus-within.d.mts +1 -1
  35. package/dist/utilities/focus-within.test.mjs +2 -1
  36. package/dist/utilities/hover.d.mts +1 -1
  37. package/dist/utilities/hover.test.mjs +2 -1
  38. package/dist/utilities/intersection-observer.test.mjs +2 -1
  39. package/dist/utilities/interval.d.mts +1 -1
  40. package/dist/utilities/interval.test.mjs +2 -1
  41. package/dist/utilities/location.d.mts +1 -1
  42. package/dist/utilities/location.test.mjs +2 -1
  43. package/dist/utilities/long-press.test.mjs +2 -1
  44. package/dist/utilities/media-devices.d.mts +1 -1
  45. package/dist/utilities/media-devices.test.mjs +2 -1
  46. package/dist/utilities/media-player.d.mts +1 -1
  47. package/dist/utilities/media-player.test.mjs +2 -1
  48. package/dist/utilities/media-query.d.mts +1 -1
  49. package/dist/utilities/mutation-observer.test.mjs +2 -1
  50. package/dist/utilities/network.d.mts +1 -1
  51. package/dist/utilities/network.test.mjs +2 -1
  52. package/dist/utilities/on-click-outside.test.mjs +2 -1
  53. package/dist/utilities/orientation.d.mts +1 -1
  54. package/dist/utilities/previous.d.mts +1 -1
  55. package/dist/utilities/previous.test.mjs +2 -1
  56. package/dist/utilities/promise.d.mts +1 -1
  57. package/dist/utilities/promise.test.mjs +2 -1
  58. package/dist/utilities/retry.test.mjs +2 -1
  59. package/dist/utilities/routing.d.mts +1 -1
  60. package/dist/utilities/routing.test.mjs +2 -1
  61. package/dist/utilities/search-params.d.mts +1 -1
  62. package/dist/utilities/search-params.test.mjs +2 -1
  63. package/dist/utilities/ssr.test.mjs +2 -1
  64. package/dist/utilities/storage.d.mts +1 -1
  65. package/dist/utilities/storage.test.mjs +2 -1
  66. package/dist/utilities/throttled.d.mts +1 -1
  67. package/dist/utilities/throttled.test.mjs +2 -1
  68. package/dist/utilities/timeout.d.mts +1 -1
  69. package/dist/utilities/timeout.test.mjs +2 -1
  70. package/dist/utilities/window-focus.d.mts +1 -1
  71. package/dist/utilities/window-size.d.mts +1 -1
  72. package/dist/utilities/window-size.test.mjs +2 -1
  73. package/package.json +1 -1
  74. /package/dist/{attributes-DILeh3-s.d.mts → attributes-3r7Diua4.d.mts} +0 -0
  75. /package/dist/{custom-elements-D5_NMNyD.d.mts → custom-elements-CBuenqVD.d.mts} +0 -0
  76. /package/dist/{magic-string.es-cTgJnTCj.mjs → magic-string.es-CXefOmkB.mjs} +0 -0
  77. /package/dist/{slot-D5iBUSAm.d.mts → slot-CfafCBOW.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { y as Computed } from "../infer-DuFY-y2b.mjs";
1
+ import { y as Computed } from "../infer-BUUWMoG9.mjs";
2
2
 
3
3
  //#region src/utilities/active-element.d.ts
4
4
  declare const activeElement: Computed<Element | null>;
@@ -1,4 +1,5 @@
1
- import { o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-BeO5pvCo.mjs";
1
+ import { f as it, l as describe } from "../benchmark.CX_oY03V-CsUg-gW0.mjs";
2
+ import { t as globalExpect } from "../test.BmQO5GaM-ZC2MPXQb.mjs";
2
3
  import { activeElement } from "./active-element.mjs";
3
4
  //#region src/utilities/active-element.test.ts
4
5
  describe("activeElement (singleton)", () => {
@@ -1,4 +1,4 @@
1
- import { b as MaybeReactive } from "../infer-DuFY-y2b.mjs";
1
+ import { b as MaybeReactive } from "../infer-BUUWMoG9.mjs";
2
2
  import { ComputedPromise } from "./promise.mjs";
3
3
 
4
4
  //#region src/utilities/async.d.ts
@@ -1,6 +1,7 @@
1
1
  import { m as signal, p as onCleanup, s as effect } from "../lib-D6duEs38.mjs";
2
2
  import "../signals/index.mjs";
3
- import { n as vi, o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-BeO5pvCo.mjs";
3
+ import { f as it, l as describe } from "../benchmark.CX_oY03V-CsUg-gW0.mjs";
4
+ import { n as vi, t as globalExpect } from "../test.BmQO5GaM-ZC2MPXQb.mjs";
4
5
  import { Async, async } from "./async.mjs";
5
6
  import { createInterval } from "./interval.mjs";
6
7
  //#region src/utilities/async.test.ts
@@ -0,0 +1,78 @@
1
+ //#region src/utilities/context.d.ts
2
+ /**
3
+ * Register `value` under `key` on `host`. Calls to {@link getContext} from
4
+ * any descendant of `host` (in the DOM tree, including across open shadow
5
+ * roots) resolve to this value, unless an inner provider with the same key
6
+ * shadows it. Returns `host` so the call can be used inline.
7
+ *
8
+ * Must be called inside an `effect` / `effectScope` (or a wrapped
9
+ * `connectedCallback`). The entry is auto-removed via `onCleanup` when the
10
+ * surrounding scope disposes. Passes `null` through unchanged.
11
+ *
12
+ * @example
13
+ * Inline form — `setContext` returns the host so it composes directly:
14
+ * ```tsx
15
+ * import { signal } from "elements-kit/signals";
16
+ * import { setContext } from "elements-kit/utilities/context";
17
+ *
18
+ * const THEME = Symbol("theme");
19
+ *
20
+ * function ThemeProvider({ children }: { children: JSX.Element }) {
21
+ * const theme = signal<"light" | "dark">("dark");
22
+ * return setContext(<div>{children}</div>, THEME, theme);
23
+ * }
24
+ * ```
25
+ *
26
+ * @example
27
+ * Equivalent `ref` form:
28
+ * ```tsx
29
+ * function ThemeProvider({ children }: { children: JSX.Element }) {
30
+ * const theme = signal<"light" | "dark">("dark");
31
+ * return (
32
+ * <div ref={(el) => setContext(el, THEME, theme)}>
33
+ * {children}
34
+ * </div>
35
+ * );
36
+ * }
37
+ * ```
38
+ */
39
+ declare function setContext<H extends EventTarget | null, T>(host: H, key: PropertyKey, value: T): H;
40
+ /**
41
+ * Walk up from `consumer` (across open shadow boundaries via
42
+ * `getRootNode().host`) and return the first registered value for `key`,
43
+ * or `undefined`.
44
+ *
45
+ * One-shot — does not subscribe to anything. If `value` is a
46
+ * `Signal`/`Computed`, the caller reads it inside their own `effect` for
47
+ * reactivity.
48
+ *
49
+ * @example
50
+ * Inside a custom element's `connectedCallback`:
51
+ * ```ts
52
+ * const theme = getContext<() => "light" | "dark">(this, THEME);
53
+ * effect(() => { this.dataset.theme = theme?.() ?? "light"; });
54
+ * ```
55
+ *
56
+ * @example
57
+ * From a JSX component — defer the lookup with `<dom-lifecycle>`, since `ref`
58
+ * runs before the element is inserted:
59
+ * ```tsx
60
+ * import { signal, type Signal } from "elements-kit/signals";
61
+ * import "elements-kit/utilities/dom-lifecycle";
62
+ *
63
+ * function ThemeConsumer() {
64
+ * const theme = signal<Signal<"light" | "dark"> | undefined>(undefined);
65
+ * return (
66
+ * <div>
67
+ * <dom-lifecycle
68
+ * onConnect={(el) => theme(getContext(el, THEME))}
69
+ * />
70
+ * {() => theme()?.() ?? "light"}
71
+ * </div>
72
+ * );
73
+ * }
74
+ * ```
75
+ */
76
+ declare function getContext<T>(consumer: Element, key: PropertyKey): T | undefined;
77
+ //#endregion
78
+ export { getContext, setContext };
@@ -0,0 +1,115 @@
1
+ import { m as signal, p as onCleanup } from "../lib-D6duEs38.mjs";
2
+ import "../signals/index.mjs";
3
+ //#region src/utilities/context.ts
4
+ const registry = /* @__PURE__ */ new WeakMap();
5
+ const versions = /* @__PURE__ */ new Map();
6
+ function version(key) {
7
+ let v = versions.get(key);
8
+ if (!v) versions.set(key, v = signal(0));
9
+ return v;
10
+ }
11
+ /**
12
+ * Register `value` under `key` on `host`. Calls to {@link getContext} from
13
+ * any descendant of `host` (in the DOM tree, including across open shadow
14
+ * roots) resolve to this value, unless an inner provider with the same key
15
+ * shadows it. Returns `host` so the call can be used inline.
16
+ *
17
+ * Must be called inside an `effect` / `effectScope` (or a wrapped
18
+ * `connectedCallback`). The entry is auto-removed via `onCleanup` when the
19
+ * surrounding scope disposes. Passes `null` through unchanged.
20
+ *
21
+ * @example
22
+ * Inline form — `setContext` returns the host so it composes directly:
23
+ * ```tsx
24
+ * import { signal } from "elements-kit/signals";
25
+ * import { setContext } from "elements-kit/utilities/context";
26
+ *
27
+ * const THEME = Symbol("theme");
28
+ *
29
+ * function ThemeProvider({ children }: { children: JSX.Element }) {
30
+ * const theme = signal<"light" | "dark">("dark");
31
+ * return setContext(<div>{children}</div>, THEME, theme);
32
+ * }
33
+ * ```
34
+ *
35
+ * @example
36
+ * Equivalent `ref` form:
37
+ * ```tsx
38
+ * function ThemeProvider({ children }: { children: JSX.Element }) {
39
+ * const theme = signal<"light" | "dark">("dark");
40
+ * return (
41
+ * <div ref={(el) => setContext(el, THEME, theme)}>
42
+ * {children}
43
+ * </div>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ function setContext(host, key, value) {
49
+ if (host == null) return host;
50
+ let map = registry.get(host);
51
+ if (!map) registry.set(host, map = /* @__PURE__ */ new Map());
52
+ map.set(key, value);
53
+ const owned = map;
54
+ const v = version(key);
55
+ v(v() + 1);
56
+ onCleanup(() => {
57
+ owned.delete(key);
58
+ if (owned.size === 0) registry.delete(host);
59
+ v(v() + 1);
60
+ });
61
+ return host;
62
+ }
63
+ /**
64
+ * Walk up from `consumer` (across open shadow boundaries via
65
+ * `getRootNode().host`) and return the first registered value for `key`,
66
+ * or `undefined`.
67
+ *
68
+ * One-shot — does not subscribe to anything. If `value` is a
69
+ * `Signal`/`Computed`, the caller reads it inside their own `effect` for
70
+ * reactivity.
71
+ *
72
+ * @example
73
+ * Inside a custom element's `connectedCallback`:
74
+ * ```ts
75
+ * const theme = getContext<() => "light" | "dark">(this, THEME);
76
+ * effect(() => { this.dataset.theme = theme?.() ?? "light"; });
77
+ * ```
78
+ *
79
+ * @example
80
+ * From a JSX component — defer the lookup with `<dom-lifecycle>`, since `ref`
81
+ * runs before the element is inserted:
82
+ * ```tsx
83
+ * import { signal, type Signal } from "elements-kit/signals";
84
+ * import "elements-kit/utilities/dom-lifecycle";
85
+ *
86
+ * function ThemeConsumer() {
87
+ * const theme = signal<Signal<"light" | "dark"> | undefined>(undefined);
88
+ * return (
89
+ * <div>
90
+ * <dom-lifecycle
91
+ * onConnect={(el) => theme(getContext(el, THEME))}
92
+ * />
93
+ * {() => theme()?.() ?? "light"}
94
+ * </div>
95
+ * );
96
+ * }
97
+ * ```
98
+ */
99
+ function getContext(consumer, key) {
100
+ version(key)();
101
+ let node = consumer;
102
+ while (node) {
103
+ const map = registry.get(node);
104
+ if (map?.has(key)) return map.get(key);
105
+ const parent = node.parentNode;
106
+ if (parent) {
107
+ node = parent;
108
+ continue;
109
+ }
110
+ const root = node.getRootNode();
111
+ node = root instanceof ShadowRoot ? root.host : null;
112
+ }
113
+ }
114
+ //#endregion
115
+ export { getContext, setContext };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,171 @@
1
+ import { c as effectScope, m as signal, s as effect } from "../lib-D6duEs38.mjs";
2
+ import "../signals/index.mjs";
3
+ import { f as it, l as describe, o as afterEach } from "../benchmark.CX_oY03V-CsUg-gW0.mjs";
4
+ import { n as vi, t as globalExpect } from "../test.BmQO5GaM-ZC2MPXQb.mjs";
5
+ import { getContext, setContext } from "./context.mjs";
6
+ //#region src/utilities/context.test.ts
7
+ afterEach(() => {
8
+ document.body.innerHTML = "";
9
+ });
10
+ describe("context", () => {
11
+ it("returns undefined when no provider exists above the consumer", () => {
12
+ const el = document.createElement("div");
13
+ document.body.appendChild(el);
14
+ globalExpect(getContext(el, "missing")).toBeUndefined();
15
+ });
16
+ it("resolves a value across one parent boundary", () => {
17
+ const host = document.createElement("section");
18
+ const child = document.createElement("span");
19
+ host.appendChild(child);
20
+ document.body.appendChild(host);
21
+ effectScope(() => {
22
+ setContext(host, "k", 42);
23
+ });
24
+ globalExpect(getContext(child, "k")).toBe(42);
25
+ });
26
+ it("propagates updates when the value is a Signal", () => {
27
+ const host = document.createElement("section");
28
+ const child = document.createElement("span");
29
+ host.appendChild(child);
30
+ document.body.appendChild(host);
31
+ const value = signal(1);
32
+ const seen = vi.fn();
33
+ effectScope(() => {
34
+ setContext(host, "n", value);
35
+ });
36
+ effect(() => {
37
+ seen(getContext(child, "n")?.());
38
+ });
39
+ globalExpect(seen).toHaveBeenLastCalledWith(1);
40
+ value(2);
41
+ globalExpect(seen).toHaveBeenLastCalledWith(2);
42
+ });
43
+ it("innermost provider wins when nested with the same key", () => {
44
+ const outer = document.createElement("section");
45
+ const inner = document.createElement("section");
46
+ const leaf = document.createElement("span");
47
+ outer.appendChild(inner);
48
+ inner.appendChild(leaf);
49
+ document.body.appendChild(outer);
50
+ effectScope(() => {
51
+ setContext(outer, "k", "outer");
52
+ setContext(inner, "k", "inner");
53
+ });
54
+ globalExpect(getContext(leaf, "k")).toBe("inner");
55
+ });
56
+ it("disposing the provider's scope removes the entry and falls through", () => {
57
+ const outer = document.createElement("section");
58
+ const inner = document.createElement("section");
59
+ const leaf = document.createElement("span");
60
+ outer.appendChild(inner);
61
+ inner.appendChild(leaf);
62
+ document.body.appendChild(outer);
63
+ const stopOuter = effectScope(() => {
64
+ setContext(outer, "k", "outer");
65
+ });
66
+ const stopInner = effectScope(() => {
67
+ setContext(inner, "k", "inner");
68
+ });
69
+ globalExpect(getContext(leaf, "k")).toBe("inner");
70
+ stopInner();
71
+ globalExpect(getContext(leaf, "k")).toBe("outer");
72
+ stopOuter();
73
+ globalExpect(getContext(leaf, "k")).toBeUndefined();
74
+ });
75
+ it("distinct keys do not cross-talk", () => {
76
+ const host = document.createElement("section");
77
+ const child = document.createElement("span");
78
+ host.appendChild(child);
79
+ document.body.appendChild(host);
80
+ const A = Symbol("a");
81
+ const B = Symbol("b");
82
+ effectScope(() => {
83
+ setContext(host, A, "value-a");
84
+ setContext(host, B, "value-b");
85
+ });
86
+ globalExpect(getContext(child, A)).toBe("value-a");
87
+ globalExpect(getContext(child, B)).toBe("value-b");
88
+ globalExpect(getContext(child, "c")).toBeUndefined();
89
+ });
90
+ it("crosses an open shadow root via getRootNode().host", () => {
91
+ const host = document.createElement("section");
92
+ const shadow = host.attachShadow({ mode: "open" });
93
+ const inner = document.createElement("span");
94
+ shadow.appendChild(inner);
95
+ document.body.appendChild(host);
96
+ effectScope(() => {
97
+ setContext(host, "k", "from-host");
98
+ });
99
+ globalExpect(getContext(inner, "k")).toBe("from-host");
100
+ });
101
+ it("moving the consumer picks up the new ancestor on next get", () => {
102
+ const a = document.createElement("section");
103
+ const b = document.createElement("section");
104
+ const leaf = document.createElement("span");
105
+ a.appendChild(leaf);
106
+ document.body.append(a, b);
107
+ effectScope(() => {
108
+ setContext(a, "k", "a");
109
+ setContext(b, "k", "b");
110
+ });
111
+ globalExpect(getContext(leaf, "k")).toBe("a");
112
+ b.appendChild(leaf);
113
+ globalExpect(getContext(leaf, "k")).toBe("b");
114
+ });
115
+ it("returns undefined for an orphan element not in any tree", () => {
116
+ globalExpect(getContext(document.createElement("div"), "k")).toBeUndefined();
117
+ });
118
+ it("two providers with different keys in the same subtree both resolve", () => {
119
+ const host = document.createElement("section");
120
+ const leaf = document.createElement("span");
121
+ host.appendChild(leaf);
122
+ document.body.appendChild(host);
123
+ effectScope(() => {
124
+ setContext(host, "x", 1);
125
+ setContext(host, "y", 2);
126
+ });
127
+ globalExpect(getContext(leaf, "x")).toBe(1);
128
+ globalExpect(getContext(leaf, "y")).toBe(2);
129
+ });
130
+ describe("with <dom-lifecycle> as wrapper", () => {
131
+ it("descendants resolve a context set on the lifecycle wrapper itself", () => {
132
+ const wrapper = document.createElement("dom-lifecycle");
133
+ const consumer = document.createElement("button");
134
+ wrapper.appendChild(consumer);
135
+ document.body.appendChild(wrapper);
136
+ effectScope(() => {
137
+ setContext(wrapper, "theme", "dark");
138
+ });
139
+ globalExpect(getContext(consumer, "theme")).toBe("dark");
140
+ });
141
+ it("the wrapper is transparent in the ancestor walk — outer providers still resolve", () => {
142
+ const provider = document.createElement("section");
143
+ document.body.appendChild(provider);
144
+ effectScope(() => {
145
+ setContext(provider, "theme", "outer");
146
+ });
147
+ const wrapper = document.createElement("dom-lifecycle");
148
+ const consumer = document.createElement("button");
149
+ wrapper.appendChild(consumer);
150
+ provider.appendChild(wrapper);
151
+ globalExpect(getContext(consumer, "theme")).toBe("outer");
152
+ });
153
+ it("the wrapper shadows an outer provider when both register the same key", () => {
154
+ const provider = document.createElement("section");
155
+ document.body.appendChild(provider);
156
+ effectScope(() => {
157
+ setContext(provider, "theme", "outer");
158
+ });
159
+ const wrapper = document.createElement("dom-lifecycle");
160
+ const consumer = document.createElement("button");
161
+ wrapper.appendChild(consumer);
162
+ provider.appendChild(wrapper);
163
+ effectScope(() => {
164
+ setContext(wrapper, "theme", "inner");
165
+ });
166
+ globalExpect(getContext(consumer, "theme")).toBe("inner");
167
+ });
168
+ });
169
+ });
170
+ //#endregion
171
+ export {};
@@ -1,4 +1,4 @@
1
- import { y as Computed } from "../infer-DuFY-y2b.mjs";
1
+ import { y as Computed } from "../infer-BUUWMoG9.mjs";
2
2
 
3
3
  //#region src/utilities/debounced.d.ts
4
4
  /**
@@ -1,6 +1,7 @@
1
1
  import { c as effectScope, m as signal } from "../lib-D6duEs38.mjs";
2
2
  import "../signals/index.mjs";
3
- import { a as beforeEach, n as vi, o as describe, r as afterEach, s as it, t as globalExpect } from "../test.BmQO5GaM-BeO5pvCo.mjs";
3
+ import { c as beforeEach, f as it, l as describe, o as afterEach } from "../benchmark.CX_oY03V-CsUg-gW0.mjs";
4
+ import { n as vi, t as globalExpect } from "../test.BmQO5GaM-ZC2MPXQb.mjs";
4
5
  import { createDebounced } from "./debounced.mjs";
5
6
  //#region src/utilities/debounced.test.ts
6
7
  beforeEach(() => {
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,90 @@
1
+ import { l as describe, t as bench } from "../benchmark.CX_oY03V-CsUg-gW0.mjs";
2
+ //#region src/utilities/dom-lifecycle.bench.ts
3
+ describe("dom-lifecycle — registration cost", () => {
4
+ bench("register 100 entries on already-connected elements", () => {
5
+ document.body.innerHTML = "";
6
+ for (let i = 0; i < 100; i++) {
7
+ const el = document.createElement("div");
8
+ document.body.appendChild(el);
9
+ const probe = document.createElement("dom-lifecycle");
10
+ probe.onConnect = () => {};
11
+ el.appendChild(probe);
12
+ }
13
+ }, { iterations: 50 });
14
+ });
15
+ describe("dom-lifecycle — connect+disconnect cycle over N pre-staged entries", () => {
16
+ for (const N of [
17
+ 10,
18
+ 100,
19
+ 1e3,
20
+ 1e4
21
+ ]) {
22
+ let wrapper;
23
+ bench(`cycle ${N} entries`, () => {
24
+ document.body.appendChild(wrapper);
25
+ wrapper.remove();
26
+ }, {
27
+ iterations: N >= 1e4 ? 30 : 50,
28
+ setup: () => {
29
+ document.body.innerHTML = "";
30
+ wrapper = document.createElement("div");
31
+ for (let i = 0; i < N; i++) {
32
+ const el = document.createElement("div");
33
+ wrapper.appendChild(el);
34
+ const probe = document.createElement("dom-lifecycle");
35
+ probe.onConnect = () => {};
36
+ probe.onDisconnect = () => {};
37
+ el.appendChild(probe);
38
+ }
39
+ },
40
+ teardown: () => {
41
+ document.body.innerHTML = "";
42
+ }
43
+ });
44
+ }
45
+ });
46
+ describe("dom-lifecycle — single mutation, N inert page entries", () => {
47
+ for (const N of [
48
+ 0,
49
+ 100,
50
+ 1e3,
51
+ 1e4
52
+ ]) {
53
+ let probe;
54
+ bench(`single mutation with ${N} other entries`, () => {
55
+ document.body.appendChild(probe);
56
+ probe.remove();
57
+ }, {
58
+ iterations: 50,
59
+ setup: () => {
60
+ document.body.innerHTML = "";
61
+ for (let i = 0; i < N; i++) {
62
+ const el = document.createElement("div");
63
+ document.body.appendChild(el);
64
+ const p = document.createElement("dom-lifecycle");
65
+ p.onConnect = () => {};
66
+ el.appendChild(p);
67
+ }
68
+ probe = document.createElement("span");
69
+ },
70
+ teardown: () => {
71
+ document.body.innerHTML = "";
72
+ }
73
+ });
74
+ }
75
+ });
76
+ describe("dom-lifecycle — single-entry connect/disconnect cycle", () => {
77
+ bench("create → append → remove → append (single entry)", () => {
78
+ document.body.innerHTML = "";
79
+ const el = document.createElement("div");
80
+ const probe = document.createElement("dom-lifecycle");
81
+ probe.onConnect = () => {};
82
+ probe.onDisconnect = () => {};
83
+ el.appendChild(probe);
84
+ document.body.appendChild(el);
85
+ el.remove();
86
+ document.body.appendChild(el);
87
+ }, { iterations: 50 });
88
+ });
89
+ //#endregion
90
+ export {};
@@ -0,0 +1,88 @@
1
+ //#region src/utilities/dom-lifecycle.d.ts
2
+ type LifecycleCallback = (self: DomLifecycleElement) => void;
3
+ type AdoptedCallback = (oldDocument: Document, newDocument: Document) => void;
4
+ /**
5
+ * Custom element backing the `<dom-lifecycle>` JSX tag. Place inside any
6
+ * element to receive lifecycle notifications for that surrounding subtree —
7
+ * `onConnect` / `onDisconnect` mirror `connectedCallback` /
8
+ * `disconnectedCallback`, `onMove` mirrors the upcoming `connectedMoveCallback`
9
+ * (fires when the element is moved with `Node.moveBefore()` instead of
10
+ * disconnect+connect), and `onAdopted` mirrors `adoptedCallback`.
11
+ *
12
+ * Position-tracking callbacks (`onConnect` / `onDisconnect` / `onMove`)
13
+ * receive the lifecycle element itself. Read `self.parentElement` for the
14
+ * surrounding element, `self.firstElementChild` / `self.children` for a
15
+ * wrapped subtree, or `self.getRootNode()` to walk through a shadow root.
16
+ * `self` is always the live element — non-null even when the lifecycle
17
+ * element is the root of a shadow tree (where `parentElement` is `null`).
18
+ *
19
+ * If you need the connect-time parent inside `onDisconnect` (the spec sets
20
+ * `parentElement` to `null` before `disconnectedCallback` runs), capture it
21
+ * yourself in `onConnect`.
22
+ *
23
+ * `onConnect` runs inside an `effectScope`, so any `onCleanup` registered
24
+ * inside it (directly or via factories like `setContext`, `on`, observers,
25
+ * etc.) fires on disconnect — before `onDisconnect` is invoked. The scope
26
+ * persists across moves (`onMove`); a fresh scope is created on every
27
+ * reconnection.
28
+ *
29
+ * Layout / a11y inert by default: `display: contents` removes the box so the
30
+ * element doesn't affect its parent's layout, and `role="none"` strips its
31
+ * implicit role from the accessibility tree. Children still participate in
32
+ * both layout and a11y. (CSS structural selectors like `:empty`,
33
+ * `:first-child`, and `:nth-child` still see the element in the DOM tree —
34
+ * place the lifecycle element where those selectors don't reach if needed.)
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <div>
39
+ * <dom-lifecycle
40
+ * onConnect={(el) => el.parentElement?.classList.add("ready")}
41
+ * onDisconnect={(el) => {
42
+ * // el.parentElement is null here per spec — stash from onConnect if needed
43
+ * }}
44
+ * />
45
+ * </div>
46
+ * ```
47
+ *
48
+ * @example
49
+ * Wrap children — read the wrapped subtree via `firstElementChild`:
50
+ * ```tsx
51
+ * <section>
52
+ * <dom-lifecycle onConnect={(el) => measure(el.firstElementChild)}>
53
+ * <h1>Title</h1>
54
+ * <p>Body</p>
55
+ * </dom-lifecycle>
56
+ * </section>
57
+ * ```
58
+ */
59
+ declare class DomLifecycleElement extends HTMLElement {
60
+ #private;
61
+ constructor();
62
+ set onConnect(fn: LifecycleCallback | null);
63
+ get onConnect(): LifecycleCallback | null;
64
+ set onDisconnect(fn: LifecycleCallback | null);
65
+ get onDisconnect(): LifecycleCallback | null;
66
+ set onMove(fn: LifecycleCallback | null);
67
+ get onMove(): LifecycleCallback | null;
68
+ set onAdopted(fn: AdoptedCallback | null);
69
+ get onAdopted(): AdoptedCallback | null;
70
+ connectedCallback(): void;
71
+ disconnectedCallback(): void;
72
+ connectedMoveCallback(): void;
73
+ adoptedCallback(oldDocument: Document, newDocument: Document): void;
74
+ }
75
+ declare global {
76
+ namespace JSX {
77
+ interface IntrinsicElements {
78
+ "dom-lifecycle": {
79
+ onConnect?: LifecycleCallback;
80
+ onDisconnect?: LifecycleCallback;
81
+ onMove?: LifecycleCallback;
82
+ onAdopted?: AdoptedCallback;
83
+ };
84
+ }
85
+ }
86
+ }
87
+ //#endregion
88
+ export { DomLifecycleElement };