@praxisjs/composables 1.2.0 → 1.2.2

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 (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/browser.d.ts +41 -18
  3. package/dist/browser.d.ts.map +1 -1
  4. package/dist/browser.js +114 -112
  5. package/dist/browser.js.map +1 -1
  6. package/dist/dom.d.ts +24 -16
  7. package/dist/dom.d.ts.map +1 -1
  8. package/dist/dom.js +75 -78
  9. package/dist/dom.js.map +1 -1
  10. package/dist/list.d.ts +4 -1
  11. package/dist/list.d.ts.map +1 -1
  12. package/dist/list.js +29 -11
  13. package/dist/list.js.map +1 -1
  14. package/dist/upload.d.ts.map +1 -1
  15. package/dist/upload.js +32 -12
  16. package/dist/upload.js.map +1 -1
  17. package/dist/utilities.d.ts +13 -1
  18. package/dist/utilities.d.ts.map +1 -1
  19. package/dist/utilities.js +88 -26
  20. package/dist/utilities.js.map +1 -1
  21. package/package.json +14 -5
  22. package/src/browser.ts +129 -113
  23. package/src/dom.ts +77 -79
  24. package/src/list.ts +29 -10
  25. package/src/upload.ts +30 -11
  26. package/src/utilities.ts +101 -24
  27. package/dist/__tests__/browser.test.d.ts +0 -2
  28. package/dist/__tests__/browser.test.d.ts.map +0 -1
  29. package/dist/__tests__/browser.test.js +0 -333
  30. package/dist/__tests__/browser.test.js.map +0 -1
  31. package/dist/__tests__/dom.test.d.ts +0 -2
  32. package/dist/__tests__/dom.test.d.ts.map +0 -1
  33. package/dist/__tests__/dom.test.js +0 -416
  34. package/dist/__tests__/dom.test.js.map +0 -1
  35. package/dist/__tests__/list.test.d.ts +0 -2
  36. package/dist/__tests__/list.test.d.ts.map +0 -1
  37. package/dist/__tests__/list.test.js +0 -156
  38. package/dist/__tests__/list.test.js.map +0 -1
  39. package/dist/__tests__/upload.test.d.ts +0 -2
  40. package/dist/__tests__/upload.test.d.ts.map +0 -1
  41. package/dist/__tests__/upload.test.js +0 -531
  42. package/dist/__tests__/upload.test.js.map +0 -1
  43. package/dist/__tests__/utilities.test.d.ts +0 -2
  44. package/dist/__tests__/utilities.test.d.ts.map +0 -1
  45. package/dist/__tests__/utilities.test.js +0 -251
  46. package/dist/__tests__/utilities.test.js.map +0 -1
  47. package/src/__tests__/browser.test.ts +0 -395
  48. package/src/__tests__/dom.test.ts +0 -515
  49. package/src/__tests__/list.test.ts +0 -210
  50. package/src/__tests__/upload.test.ts +0 -639
  51. package/src/__tests__/utilities.test.ts +0 -327
  52. package/tsconfig.json +0 -8
@@ -1,515 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
-
4
- import { WindowSize, ScrollPosition, ElementSize, Intersection, Focus } from "../dom";
5
-
6
- // ── WindowSize ────────────────────────────────────────────────────────────────
7
-
8
- describe("WindowSize", () => {
9
- it("reads initial window dimensions", () => {
10
- Object.defineProperty(window, "innerWidth", { value: 1024, configurable: true });
11
- Object.defineProperty(window, "innerHeight", { value: 768, configurable: true });
12
-
13
- const ws = new WindowSize();
14
- const { width, height } = ws.setup() as { width: () => number; height: () => number };
15
- expect(width()).toBe(1024);
16
- expect(height()).toBe(768);
17
- });
18
-
19
- it("updates on resize event", () => {
20
- Object.defineProperty(window, "innerWidth", { value: 800, configurable: true });
21
- Object.defineProperty(window, "innerHeight", { value: 600, configurable: true });
22
-
23
- const ws = new WindowSize();
24
- const { width, height } = ws.setup() as { width: () => number; height: () => number };
25
-
26
- Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
27
- Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
28
- window.dispatchEvent(new Event("resize"));
29
-
30
- expect(width()).toBe(1280);
31
- expect(height()).toBe(900);
32
- });
33
-
34
- it("removes listener on unmount", () => {
35
- const remove = vi.spyOn(window, "removeEventListener");
36
- const ws = new WindowSize();
37
- ws.setup();
38
- ws.onUnmount();
39
- expect(remove).toHaveBeenCalledWith("resize", expect.any(Function));
40
- });
41
- });
42
-
43
- // ── ScrollPosition ────────────────────────────────────────────────────────────
44
-
45
- describe("ScrollPosition", () => {
46
- it("starts at (0, 0)", () => {
47
- const sp = new ScrollPosition(window);
48
- const { x, y } = sp.setup() as { x: () => number; y: () => number };
49
- expect(x()).toBe(0);
50
- expect(y()).toBe(0);
51
- });
52
-
53
- it("updates on scroll event for window", () => {
54
- const sp = new ScrollPosition(window);
55
- const { x, y } = sp.setup() as { x: () => number; y: () => number };
56
- sp.onMount();
57
-
58
- Object.defineProperty(window, "scrollX", { value: 100, configurable: true });
59
- Object.defineProperty(window, "scrollY", { value: 200, configurable: true });
60
- window.dispatchEvent(new Event("scroll"));
61
-
62
- expect(x()).toBe(100);
63
- expect(y()).toBe(200);
64
- });
65
-
66
- it("updates on scroll event for element", () => {
67
- const el = document.createElement("div");
68
- const sp = new ScrollPosition(el);
69
- const { x, y } = sp.setup() as { x: () => number; y: () => number };
70
- sp.onMount();
71
-
72
- Object.defineProperty(el, "scrollLeft", { value: 50, configurable: true });
73
- Object.defineProperty(el, "scrollTop", { value: 75, configurable: true });
74
- el.dispatchEvent(new Event("scroll"));
75
-
76
- expect(x()).toBe(50);
77
- expect(y()).toBe(75);
78
- });
79
-
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.onMount();
86
- sp.onUnmount();
87
- expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
88
- });
89
- });
90
-
91
- // ── ElementSize ───────────────────────────────────────────────────────────────
92
-
93
- describe("ElementSize", () => {
94
- let observerCallback: ResizeObserverCallback;
95
-
96
- beforeEach(() => {
97
- vi.stubGlobal("ResizeObserver", class {
98
- constructor(cb: ResizeObserverCallback) { observerCallback = cb; }
99
- observe = vi.fn();
100
- disconnect = vi.fn();
101
- });
102
- });
103
-
104
- it("starts at (0, 0) with no element", () => {
105
- const ref = { current: null };
106
- const es = new ElementSize(ref);
107
- const { width, height } = es.setup() as { width: () => number; height: () => number };
108
- expect(width()).toBe(0);
109
- expect(height()).toBe(0);
110
- });
111
-
112
- it("updates via ResizeObserver callback", () => {
113
- const el = document.createElement("div");
114
- const ref = { current: el };
115
- const es = new ElementSize(ref);
116
- const { width, height } = es.setup() as { width: () => number; height: () => number };
117
-
118
- observerCallback(
119
- [{ contentRect: { width: 300, height: 150 } } as ResizeObserverEntry],
120
- {} as ResizeObserver,
121
- );
122
-
123
- expect(width()).toBe(300);
124
- expect(height()).toBe(150);
125
- });
126
-
127
- it("disconnects observer on unmount", () => {
128
- const disconnect = vi.fn();
129
- vi.stubGlobal("ResizeObserver", class {
130
- constructor(cb: ResizeObserverCallback) { observerCallback = cb; }
131
- observe = vi.fn();
132
- disconnect = disconnect;
133
- });
134
-
135
- const ref = { current: null };
136
- const es = new ElementSize(ref);
137
- es.setup();
138
- es.onUnmount();
139
- expect(disconnect).toHaveBeenCalled();
140
- });
141
-
142
- it("onMount() with null ref is a no-op", () => {
143
- const ref = { current: null };
144
- const es = new ElementSize(ref);
145
- es.setup();
146
- expect(() => es.onMount()).not.toThrow();
147
- });
148
- });
149
-
150
- // ── Intersection ──────────────────────────────────────────────────────────────
151
-
152
- describe("Intersection", () => {
153
- let observerCallback: IntersectionObserverCallback;
154
-
155
- beforeEach(() => {
156
- vi.stubGlobal("IntersectionObserver", class {
157
- constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
158
- observe = vi.fn();
159
- disconnect = vi.fn();
160
- });
161
- });
162
-
163
- it("starts as not visible", () => {
164
- const ref = { current: null };
165
- const int = new Intersection(ref);
166
- const { visible } = int.setup() as { visible: () => boolean };
167
- expect(visible()).toBe(false);
168
- });
169
-
170
- it("becomes visible when intersecting", () => {
171
- const el = document.createElement("div");
172
- const ref = { current: el };
173
- const int = new Intersection(ref);
174
- const { visible } = int.setup() as { visible: () => boolean };
175
-
176
- observerCallback(
177
- [{ isIntersecting: true } as IntersectionObserverEntry],
178
- {} as IntersectionObserver,
179
- );
180
-
181
- expect(visible()).toBe(true);
182
- });
183
-
184
- it("disconnects on unmount", () => {
185
- const disconnect = vi.fn();
186
- vi.stubGlobal("IntersectionObserver", class {
187
- constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
188
- observe = vi.fn();
189
- disconnect = disconnect;
190
- });
191
-
192
- const ref = { current: null };
193
- const int = new Intersection(ref);
194
- int.setup();
195
- int.onUnmount();
196
- expect(disconnect).toHaveBeenCalled();
197
- });
198
-
199
- it("onMount() calls observe() with the element", () => {
200
- const observeFn = vi.fn();
201
- vi.stubGlobal("IntersectionObserver", class {
202
- constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
203
- observe = observeFn;
204
- disconnect = vi.fn();
205
- });
206
-
207
- const el = document.createElement("div");
208
- const ref = { current: el };
209
- const int = new Intersection(ref);
210
- int.setup();
211
- int.onMount();
212
-
213
- expect(observeFn).toHaveBeenCalledWith(el);
214
- });
215
-
216
- it("onMount() does nothing when ref is null", () => {
217
- const observeFn = vi.fn();
218
- vi.stubGlobal("IntersectionObserver", class {
219
- constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
220
- observe = observeFn;
221
- disconnect = vi.fn();
222
- });
223
-
224
- const ref = { current: null };
225
- const int = new Intersection(ref);
226
- int.setup();
227
- expect(() => int.onMount()).not.toThrow();
228
- expect(observeFn).not.toHaveBeenCalled();
229
- });
230
- });
231
-
232
- // ── ElementSize (additional) ──────────────────────────────────────────────────
233
-
234
- describe("ElementSize (additional)", () => {
235
- it("ref.current changes to a new element: old observer is disconnected and new one started", () => {
236
- const disconnectFn = vi.fn();
237
- const observeFn = vi.fn();
238
-
239
- vi.stubGlobal("ResizeObserver", class {
240
- observe = observeFn;
241
- disconnect = disconnectFn;
242
- });
243
-
244
- const el1 = document.createElement("div");
245
- const ref = { current: el1 as HTMLElement | null };
246
- const es = new ElementSize(ref);
247
- es.setup();
248
- es.onMount();
249
-
250
- // Disconnect old observer and create new one for el2
251
- es.onUnmount();
252
- expect(disconnectFn).toHaveBeenCalled();
253
-
254
- const el2 = document.createElement("div");
255
- ref.current = el2;
256
- const es2 = new ElementSize(ref);
257
- es2.setup();
258
- es2.onMount();
259
- expect(observeFn).toHaveBeenCalledWith(el2);
260
- });
261
- });
262
-
263
- // ── Intersection (additional) ─────────────────────────────────────────────────
264
-
265
- describe("Intersection (additional)", () => {
266
- let observerCallback: IntersectionObserverCallback;
267
-
268
- beforeEach(() => {
269
- vi.stubGlobal("IntersectionObserver", class {
270
- constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
271
- observe = vi.fn();
272
- disconnect = vi.fn();
273
- });
274
- });
275
-
276
- it("visibility toggles false after being true", () => {
277
- const el = document.createElement("div");
278
- const ref = { current: el };
279
- const int = new Intersection(ref);
280
- const { visible } = int.setup() as { visible: () => boolean };
281
-
282
- observerCallback(
283
- [{ isIntersecting: true } as IntersectionObserverEntry],
284
- {} as IntersectionObserver,
285
- );
286
- expect(visible()).toBe(true);
287
-
288
- observerCallback(
289
- [{ isIntersecting: false } as IntersectionObserverEntry],
290
- {} as IntersectionObserver,
291
- );
292
- expect(visible()).toBe(false);
293
- });
294
- });
295
-
296
- // ── ScrollPosition (ref-based) ────────────────────────────────────────────────
297
-
298
- describe("ScrollPosition (ref-based)", () => {
299
- it("accepts a ref object and reads scrollLeft/scrollTop from it", () => {
300
- const el = document.createElement("div");
301
- const ref = { current: el };
302
- const sp = new ScrollPosition(ref);
303
- const { x, y } = sp.setup() as { x: () => number; y: () => number };
304
- sp.onMount();
305
-
306
- Object.defineProperty(el, "scrollLeft", { value: 30, configurable: true });
307
- Object.defineProperty(el, "scrollTop", { value: 60, configurable: true });
308
- el.dispatchEvent(new Event("scroll"));
309
-
310
- expect(x()).toBe(30);
311
- expect(y()).toBe(60);
312
- });
313
-
314
- it("uses ref.current at mount time — subsequent current changes do not affect the listener target", () => {
315
- const el = document.createElement("div");
316
- const ref = { current: el as HTMLElement | null };
317
- const sp = new ScrollPosition(ref);
318
- sp.setup();
319
- sp.onMount();
320
-
321
- const remove = vi.spyOn(el, "removeEventListener");
322
- sp.onUnmount();
323
- expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
324
- });
325
-
326
- it("falls back to window when ref.current is null at mount time", () => {
327
- const ref = { current: null as HTMLElement | null };
328
- const sp = new ScrollPosition(ref);
329
- sp.setup();
330
- // Should not throw; _target falls back to window
331
- expect(() => sp.onMount()).not.toThrow();
332
- sp.onUnmount();
333
- });
334
- });
335
-
336
- // ── ScrollPosition (additional) ───────────────────────────────────────────────
337
-
338
- describe("ScrollPosition (additional)", () => {
339
- it("onMount() called twice registers only one scroll listener", () => {
340
- const el = document.createElement("div");
341
- const add = vi.spyOn(el, "addEventListener");
342
- const sp = new ScrollPosition(el);
343
- sp.setup();
344
- sp.onMount();
345
- sp.onMount(); // second call must be a no-op
346
- const scrollCalls = add.mock.calls.filter((c) => c[0] === "scroll").length;
347
- expect(scrollCalls).toBe(1);
348
- });
349
- });
350
-
351
- // ── Focus ─────────────────────────────────────────────────────────────────────
352
-
353
- describe("Focus", () => {
354
- it("starts as not focused", () => {
355
- const el = document.createElement("input");
356
- const ref = { current: el };
357
- const focus = new Focus(ref);
358
- const { focused } = focus.setup() as { focused: () => boolean };
359
- expect(focused()).toBe(false);
360
- });
361
-
362
- it("sets focused=true on focus event", () => {
363
- const el = document.createElement("input");
364
- document.body.appendChild(el);
365
- const ref = { current: el };
366
- const focus = new Focus(ref);
367
- const { focused } = focus.setup() as { focused: () => boolean };
368
- focus.onMount();
369
-
370
- el.dispatchEvent(new Event("focus"));
371
- expect(focused()).toBe(true);
372
-
373
- document.body.removeChild(el);
374
- });
375
-
376
- it("sets focused=false on blur event", () => {
377
- const el = document.createElement("input");
378
- document.body.appendChild(el);
379
- const ref = { current: el };
380
- const focus = new Focus(ref);
381
- const { focused } = focus.setup() as { focused: () => boolean };
382
- focus.onMount();
383
-
384
- el.dispatchEvent(new Event("focus"));
385
- el.dispatchEvent(new Event("blur"));
386
- expect(focused()).toBe(false);
387
-
388
- document.body.removeChild(el);
389
- });
390
-
391
- it("does not throw when ref is null", () => {
392
- const ref = { current: null };
393
- const focus = new Focus(ref);
394
- expect(() => focus.setup()).not.toThrow();
395
- });
396
-
397
- it("onMount() with null ref is a no-op", () => {
398
- const ref = { current: null };
399
- const focus = new Focus(ref);
400
- focus.setup();
401
- expect(() => focus.onMount()).not.toThrow();
402
- });
403
-
404
- it("onUnmount() with null ref is a no-op", () => {
405
- const ref = { current: null };
406
- const focus = new Focus(ref);
407
- focus.setup();
408
- expect(() => focus.onUnmount()).not.toThrow();
409
- });
410
-
411
- it("removes focus/blur listeners on unmount", () => {
412
- const el = document.createElement("input");
413
- document.body.appendChild(el);
414
- const ref = { current: el };
415
- const focus = new Focus(ref);
416
- focus.setup();
417
- focus.onMount();
418
-
419
- const remove = vi.spyOn(el, "removeEventListener");
420
- focus.onUnmount();
421
-
422
- expect(remove).toHaveBeenCalledWith("focus", expect.any(Function));
423
- expect(remove).toHaveBeenCalledWith("blur", expect.any(Function));
424
-
425
- document.body.removeChild(el);
426
- });
427
-
428
- it("onMount() called twice registers only one listener pair", () => {
429
- const el = document.createElement("input");
430
- document.body.appendChild(el);
431
- const add = vi.spyOn(el, "addEventListener");
432
- const ref = { current: el };
433
- const focus = new Focus(ref);
434
- focus.setup();
435
- focus.onMount();
436
- focus.onMount(); // second call must be a no-op
437
- const focusCalls = add.mock.calls.filter((c) => c[0] === "focus").length;
438
- expect(focusCalls).toBe(1);
439
- document.body.removeChild(el);
440
- });
441
- });
442
-
443
- // ── WindowSize (additional) ───────────────────────────────────────────────────
444
-
445
- describe("WindowSize (additional)", () => {
446
- it("setup() called twice returns the same view object", () => {
447
- const ws = new WindowSize();
448
- const view1 = ws.setup();
449
- const view2 = ws.setup();
450
- expect(view1).toBe(view2);
451
- ws.onUnmount();
452
- });
453
-
454
- it("setup() called twice registers only one resize listener", () => {
455
- const add = vi.spyOn(window, "addEventListener");
456
- const ws = new WindowSize();
457
- ws.setup();
458
- ws.setup();
459
- const resizeCalls = add.mock.calls.filter((c) => c[0] === "resize").length;
460
- expect(resizeCalls).toBe(1);
461
- ws.onUnmount();
462
- vi.restoreAllMocks();
463
- });
464
- });
465
-
466
- // ── ElementSize (cache) ───────────────────────────────────────────────────────
467
-
468
- describe("ElementSize (cache)", () => {
469
- it("setup() called twice returns the same view object", () => {
470
- const el = document.createElement("div");
471
- document.body.appendChild(el);
472
- const ref = { current: el };
473
- const es = new ElementSize(ref);
474
- const view1 = es.setup();
475
- const view2 = es.setup();
476
- expect(view1).toBe(view2);
477
- es.onUnmount();
478
- document.body.removeChild(el);
479
- });
480
- });
481
-
482
- // ── Intersection (cache) ──────────────────────────────────────────────────────
483
-
484
- describe("Intersection (cache)", () => {
485
- it("setup() called twice returns the same view object", () => {
486
- const el = document.createElement("div");
487
- document.body.appendChild(el);
488
- const ref = { current: el };
489
- const inter = new Intersection(ref);
490
- const view1 = inter.setup();
491
- const view2 = inter.setup();
492
- expect(view1).toBe(view2);
493
- inter.onUnmount();
494
- document.body.removeChild(el);
495
- });
496
- });
497
-
498
- // ── ScrollPosition (_staticTarget ?? window fallback) ─────────────────────────
499
-
500
- describe("ScrollPosition (_staticTarget fallback)", () => {
501
- it("falls back to window when _staticTarget is undefined", () => {
502
- // Default constructor sets _staticTarget = window. After construction we
503
- // forcibly clear it to undefined (bypassing TypeScript's `private readonly`)
504
- // so _resolveTarget() exercises the final `return window` branch.
505
- const sp = new ScrollPosition();
506
- (sp as unknown as Record<string, unknown>)._staticTarget = undefined;
507
- sp.setup();
508
- const addSpy = vi.spyOn(window, "addEventListener");
509
- sp.onMount();
510
- const scrollCall = addSpy.mock.calls.find((c) => c[0] === "scroll");
511
- expect(scrollCall).toBeDefined(); // proves _resolveTarget() hit the window fallback
512
- addSpy.mockRestore();
513
- sp.onUnmount();
514
- });
515
- });