@praxisjs/composables 0.1.5 → 1.0.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.
- package/CHANGELOG.md +47 -0
- package/dist/__tests__/browser.test.js +151 -82
- package/dist/__tests__/browser.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +168 -124
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/utilities.test.js +155 -193
- package/dist/__tests__/utilities.test.js.map +1 -1
- package/dist/browser.d.ts +55 -12
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +105 -49
- package/dist/browser.js.map +1 -1
- package/dist/dom.d.ts +60 -25
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +108 -74
- package/dist/dom.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/utilities.d.ts +68 -26
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +102 -94
- package/dist/utilities.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/browser.test.ts +172 -92
- package/src/__tests__/dom.test.ts +184 -135
- package/src/__tests__/utilities.test.ts +185 -213
- package/src/browser.ts +134 -53
- package/src/dom.ts +129 -82
- package/src/index.ts +11 -18
- package/src/utilities.ts +144 -111
|
@@ -1,212 +1,261 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import {
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { WindowSize, ScrollPosition, ElementSize, Intersection, Focus } from "../dom";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
it("starts with current = null", () => {
|
|
10
|
-
const ref = createRef();
|
|
11
|
-
expect(ref.current).toBeNull();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("can be assigned an element", () => {
|
|
15
|
-
const ref = createRef();
|
|
16
|
-
const el = document.createElement("div");
|
|
17
|
-
ref.current = el;
|
|
18
|
-
expect(ref.current).toBe(el);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// ---------- useWindowSize ----------
|
|
8
|
+
// ── WindowSize ────────────────────────────────────────────────────────────────
|
|
23
9
|
|
|
24
|
-
describe("
|
|
25
|
-
it("
|
|
26
|
-
Object.defineProperty(window, "innerWidth", {
|
|
27
|
-
Object.defineProperty(window, "innerHeight", {
|
|
10
|
+
describe("WindowSize", () => {
|
|
11
|
+
it("reads initial window dimensions", () => {
|
|
12
|
+
Object.defineProperty(window, "innerWidth", { value: 1024, configurable: true });
|
|
13
|
+
Object.defineProperty(window, "innerHeight", { value: 768, configurable: true });
|
|
28
14
|
|
|
29
|
-
const
|
|
15
|
+
const ws = new WindowSize();
|
|
16
|
+
const { width, height } = ws.setup() as { width: () => number; height: () => number };
|
|
30
17
|
expect(width()).toBe(1024);
|
|
31
18
|
expect(height()).toBe(768);
|
|
32
19
|
});
|
|
33
20
|
|
|
34
21
|
it("updates on resize event", () => {
|
|
35
|
-
Object.defineProperty(window, "innerWidth", {
|
|
36
|
-
Object.defineProperty(window, "innerHeight", {
|
|
22
|
+
Object.defineProperty(window, "innerWidth", { value: 800, configurable: true });
|
|
23
|
+
Object.defineProperty(window, "innerHeight", { value: 600, configurable: true });
|
|
37
24
|
|
|
38
|
-
const
|
|
25
|
+
const ws = new WindowSize();
|
|
26
|
+
const { width, height } = ws.setup() as { width: () => number; height: () => number };
|
|
39
27
|
|
|
40
|
-
Object.defineProperty(window, "innerWidth", {
|
|
41
|
-
Object.defineProperty(window, "innerHeight", {
|
|
28
|
+
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
|
29
|
+
Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
|
|
42
30
|
window.dispatchEvent(new Event("resize"));
|
|
43
31
|
|
|
44
32
|
expect(width()).toBe(1280);
|
|
45
|
-
expect(height()).toBe(
|
|
33
|
+
expect(height()).toBe(900);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("removes listener on unmount", () => {
|
|
37
|
+
const remove = vi.spyOn(window, "removeEventListener");
|
|
38
|
+
const ws = new WindowSize();
|
|
39
|
+
ws.setup();
|
|
40
|
+
ws.onUnmount();
|
|
41
|
+
expect(remove).toHaveBeenCalledWith("resize", expect.any(Function));
|
|
46
42
|
});
|
|
47
43
|
});
|
|
48
44
|
|
|
49
|
-
//
|
|
45
|
+
// ── ScrollPosition ────────────────────────────────────────────────────────────
|
|
50
46
|
|
|
51
|
-
describe("
|
|
47
|
+
describe("ScrollPosition", () => {
|
|
52
48
|
it("starts at (0, 0)", () => {
|
|
53
|
-
const
|
|
49
|
+
const sp = new ScrollPosition(window);
|
|
50
|
+
const { x, y } = sp.setup() as { x: () => number; y: () => number };
|
|
54
51
|
expect(x()).toBe(0);
|
|
55
52
|
expect(y()).toBe(0);
|
|
56
53
|
});
|
|
57
54
|
|
|
58
|
-
it("updates on
|
|
59
|
-
const
|
|
60
|
-
const { x, y } =
|
|
55
|
+
it("updates on scroll event for window", () => {
|
|
56
|
+
const sp = new ScrollPosition(window);
|
|
57
|
+
const { x, y } = sp.setup() as { x: () => number; y: () => number };
|
|
61
58
|
|
|
62
|
-
Object.defineProperty(
|
|
63
|
-
Object.defineProperty(
|
|
64
|
-
|
|
59
|
+
Object.defineProperty(window, "scrollX", { value: 100, configurable: true });
|
|
60
|
+
Object.defineProperty(window, "scrollY", { value: 200, configurable: true });
|
|
61
|
+
window.dispatchEvent(new Event("scroll"));
|
|
65
62
|
|
|
66
|
-
expect(x()).toBe(
|
|
67
|
-
expect(y()).toBe(
|
|
63
|
+
expect(x()).toBe(100);
|
|
64
|
+
expect(y()).toBe(200);
|
|
68
65
|
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// ---------- useFocus ----------
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const ref = { current: el };
|
|
78
|
-
const focused = useFocus(ref);
|
|
79
|
-
expect(focused()).toBe(false);
|
|
80
|
-
document.body.removeChild(el);
|
|
81
|
-
});
|
|
67
|
+
it("updates on scroll event for element", () => {
|
|
68
|
+
const el = document.createElement("div");
|
|
69
|
+
const sp = new ScrollPosition(el);
|
|
70
|
+
const { x, y } = sp.setup() as { x: () => number; y: () => number };
|
|
82
71
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const ref = { current: el };
|
|
87
|
-
const focused = useFocus(ref);
|
|
88
|
-
el.dispatchEvent(new FocusEvent("focus"));
|
|
89
|
-
expect(focused()).toBe(true);
|
|
90
|
-
document.body.removeChild(el);
|
|
91
|
-
});
|
|
72
|
+
Object.defineProperty(el, "scrollLeft", { value: 50, configurable: true });
|
|
73
|
+
Object.defineProperty(el, "scrollTop", { value: 75, configurable: true });
|
|
74
|
+
el.dispatchEvent(new Event("scroll"));
|
|
92
75
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
document.body.appendChild(el);
|
|
96
|
-
const ref = { current: el };
|
|
97
|
-
const focused = useFocus(ref);
|
|
98
|
-
el.dispatchEvent(new FocusEvent("focus"));
|
|
99
|
-
el.dispatchEvent(new FocusEvent("blur"));
|
|
100
|
-
expect(focused()).toBe(false);
|
|
101
|
-
document.body.removeChild(el);
|
|
76
|
+
expect(x()).toBe(50);
|
|
77
|
+
expect(y()).toBe(75);
|
|
102
78
|
});
|
|
103
79
|
|
|
104
|
-
it("
|
|
105
|
-
const
|
|
106
|
-
|
|
80
|
+
it("removes listener on unmount", () => {
|
|
81
|
+
const el = document.createElement("div");
|
|
82
|
+
const remove = vi.spyOn(el, "removeEventListener");
|
|
83
|
+
const sp = new ScrollPosition(el);
|
|
84
|
+
sp.setup();
|
|
85
|
+
sp.onUnmount();
|
|
86
|
+
expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
|
|
107
87
|
});
|
|
108
88
|
});
|
|
109
89
|
|
|
110
|
-
//
|
|
90
|
+
// ── ElementSize ───────────────────────────────────────────────────────────────
|
|
111
91
|
|
|
112
|
-
describe("
|
|
113
|
-
let observerCallback: ResizeObserverCallback
|
|
114
|
-
let mockDisconnect: ReturnType<typeof vi.fn>;
|
|
92
|
+
describe("ElementSize", () => {
|
|
93
|
+
let observerCallback: ResizeObserverCallback;
|
|
115
94
|
|
|
116
95
|
beforeEach(() => {
|
|
117
|
-
|
|
118
|
-
observerCallback = () => {};
|
|
119
|
-
|
|
120
|
-
// Class-based mock so `new ResizeObserver(cb)` works correctly
|
|
121
|
-
globalThis.ResizeObserver = class {
|
|
96
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
122
97
|
constructor(cb: ResizeObserverCallback) { observerCallback = cb; }
|
|
123
98
|
observe = vi.fn();
|
|
124
|
-
disconnect =
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
afterEach(() => {
|
|
129
|
-
// @ts-expect-error – removing test stub
|
|
130
|
-
delete globalThis.ResizeObserver;
|
|
99
|
+
disconnect = vi.fn();
|
|
100
|
+
});
|
|
131
101
|
});
|
|
132
102
|
|
|
133
|
-
it("starts at (0, 0)
|
|
134
|
-
const ref
|
|
135
|
-
const
|
|
103
|
+
it("starts at (0, 0) with no element", () => {
|
|
104
|
+
const ref = { current: null };
|
|
105
|
+
const es = new ElementSize(ref);
|
|
106
|
+
const { width, height } = es.setup() as { width: () => number; height: () => number };
|
|
136
107
|
expect(width()).toBe(0);
|
|
137
108
|
expect(height()).toBe(0);
|
|
138
109
|
});
|
|
139
110
|
|
|
140
|
-
it("
|
|
111
|
+
it("updates via ResizeObserver callback", () => {
|
|
141
112
|
const el = document.createElement("div");
|
|
142
113
|
const ref = { current: el };
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
expect(mockDisconnect).toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("updates width and height when ResizeObserver fires", () => {
|
|
149
|
-
const el = document.createElement("div");
|
|
150
|
-
const ref = { current: el };
|
|
151
|
-
const { width, height } = useElementSize(ref);
|
|
114
|
+
const es = new ElementSize(ref);
|
|
115
|
+
const { width, height } = es.setup() as { width: () => number; height: () => number };
|
|
152
116
|
|
|
153
117
|
observerCallback(
|
|
154
|
-
[{ contentRect: { width: 300, height:
|
|
155
|
-
|
|
118
|
+
[{ contentRect: { width: 300, height: 150 } } as ResizeObserverEntry],
|
|
119
|
+
{} as ResizeObserver,
|
|
156
120
|
);
|
|
157
121
|
|
|
158
122
|
expect(width()).toBe(300);
|
|
159
|
-
expect(height()).toBe(
|
|
123
|
+
expect(height()).toBe(150);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("disconnects observer on unmount", () => {
|
|
127
|
+
const disconnect = vi.fn();
|
|
128
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
129
|
+
constructor(cb: ResizeObserverCallback) { observerCallback = cb; }
|
|
130
|
+
observe = vi.fn();
|
|
131
|
+
disconnect = disconnect;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const ref = { current: null };
|
|
135
|
+
const es = new ElementSize(ref);
|
|
136
|
+
es.setup();
|
|
137
|
+
es.onUnmount();
|
|
138
|
+
expect(disconnect).toHaveBeenCalled();
|
|
160
139
|
});
|
|
161
140
|
});
|
|
162
141
|
|
|
163
|
-
//
|
|
142
|
+
// ── Intersection ──────────────────────────────────────────────────────────────
|
|
164
143
|
|
|
165
|
-
describe("
|
|
166
|
-
let
|
|
167
|
-
let capturedOptions: IntersectionObserverInit | undefined;
|
|
144
|
+
describe("Intersection", () => {
|
|
145
|
+
let observerCallback: IntersectionObserverCallback;
|
|
168
146
|
|
|
169
147
|
beforeEach(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
globalThis.IntersectionObserver = class {
|
|
174
|
-
constructor(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) {
|
|
175
|
-
intersectionCallback = cb;
|
|
176
|
-
capturedOptions = opts;
|
|
177
|
-
}
|
|
148
|
+
vi.stubGlobal("IntersectionObserver", class {
|
|
149
|
+
constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
|
|
178
150
|
observe = vi.fn();
|
|
179
151
|
disconnect = vi.fn();
|
|
180
|
-
}
|
|
152
|
+
});
|
|
181
153
|
});
|
|
182
154
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
it("starts as false when ref.current is null", () => {
|
|
189
|
-
const ref: { current: HTMLElement | null } = { current: null };
|
|
190
|
-
const visible = useIntersection(ref);
|
|
155
|
+
it("starts as not visible", () => {
|
|
156
|
+
const ref = { current: null };
|
|
157
|
+
const int = new Intersection(ref);
|
|
158
|
+
const { visible } = int.setup() as { visible: () => boolean };
|
|
191
159
|
expect(visible()).toBe(false);
|
|
192
160
|
});
|
|
193
161
|
|
|
194
|
-
it("becomes
|
|
162
|
+
it("becomes visible when intersecting", () => {
|
|
195
163
|
const el = document.createElement("div");
|
|
196
164
|
const ref = { current: el };
|
|
197
|
-
const
|
|
165
|
+
const int = new Intersection(ref);
|
|
166
|
+
const { visible } = int.setup() as { visible: () => boolean };
|
|
198
167
|
|
|
199
|
-
|
|
168
|
+
observerCallback(
|
|
200
169
|
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
201
|
-
|
|
170
|
+
{} as IntersectionObserver,
|
|
202
171
|
);
|
|
172
|
+
|
|
203
173
|
expect(visible()).toBe(true);
|
|
204
174
|
});
|
|
205
175
|
|
|
206
|
-
it("
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
176
|
+
it("disconnects on unmount", () => {
|
|
177
|
+
const disconnect = vi.fn();
|
|
178
|
+
vi.stubGlobal("IntersectionObserver", class {
|
|
179
|
+
constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
|
|
180
|
+
observe = vi.fn();
|
|
181
|
+
disconnect = disconnect;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const ref = { current: null };
|
|
185
|
+
const int = new Intersection(ref);
|
|
186
|
+
int.setup();
|
|
187
|
+
int.onUnmount();
|
|
188
|
+
expect(disconnect).toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── Focus ─────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("Focus", () => {
|
|
195
|
+
it("starts as not focused", () => {
|
|
196
|
+
const el = document.createElement("input");
|
|
197
|
+
const ref = { current: el };
|
|
198
|
+
const focus = new Focus(ref);
|
|
199
|
+
const { focused } = focus.setup() as { focused: () => boolean };
|
|
200
|
+
expect(focused()).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("sets focused=true on focus event", () => {
|
|
204
|
+
const el = document.createElement("input");
|
|
205
|
+
document.body.appendChild(el);
|
|
206
|
+
const ref = { current: el };
|
|
207
|
+
const focus = new Focus(ref);
|
|
208
|
+
const { focused } = focus.setup() as { focused: () => boolean };
|
|
209
|
+
|
|
210
|
+
el.dispatchEvent(new Event("focus"));
|
|
211
|
+
expect(focused()).toBe(true);
|
|
212
|
+
|
|
213
|
+
document.body.removeChild(el);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("sets focused=false on blur event", () => {
|
|
217
|
+
const el = document.createElement("input");
|
|
218
|
+
document.body.appendChild(el);
|
|
219
|
+
const ref = { current: el };
|
|
220
|
+
const focus = new Focus(ref);
|
|
221
|
+
const { focused } = focus.setup() as { focused: () => boolean };
|
|
222
|
+
|
|
223
|
+
el.dispatchEvent(new Event("focus"));
|
|
224
|
+
el.dispatchEvent(new Event("blur"));
|
|
225
|
+
expect(focused()).toBe(false);
|
|
226
|
+
|
|
227
|
+
document.body.removeChild(el);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not throw when ref is null", () => {
|
|
231
|
+
const ref = { current: null };
|
|
232
|
+
const focus = new Focus(ref);
|
|
233
|
+
expect(() => focus.setup()).not.toThrow();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("removes focus/blur listeners when reactive ref changes (effect cleanup)", () => {
|
|
237
|
+
const el1 = document.createElement("input");
|
|
238
|
+
document.body.appendChild(el1);
|
|
239
|
+
|
|
240
|
+
// Make ref.current reactive so the effect tracks it and re-runs on change
|
|
241
|
+
const refSignal = signal<HTMLElement | null>(el1);
|
|
242
|
+
const ref = { get current() { return refSignal(); } };
|
|
243
|
+
|
|
244
|
+
const focus = new Focus(ref);
|
|
245
|
+
focus.setup();
|
|
246
|
+
|
|
247
|
+
const removeEl1 = vi.spyOn(el1, "removeEventListener");
|
|
248
|
+
|
|
249
|
+
// Changing the signal causes the effect to re-run, calling cleanup for el1
|
|
250
|
+
// This covers the cleanup function at dom.ts:134-135
|
|
251
|
+
const el2 = document.createElement("input");
|
|
252
|
+
document.body.appendChild(el2);
|
|
253
|
+
refSignal.set(el2);
|
|
254
|
+
|
|
255
|
+
expect(removeEl1).toHaveBeenCalledWith("focus", expect.any(Function));
|
|
256
|
+
expect(removeEl1).toHaveBeenCalledWith("blur", expect.any(Function));
|
|
257
|
+
|
|
258
|
+
document.body.removeChild(el1);
|
|
259
|
+
document.body.removeChild(el2);
|
|
211
260
|
});
|
|
212
261
|
});
|