@praxisjs/composables 1.0.5 → 1.1.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 +82 -9
- package/dist/__tests__/browser.test.js +7 -0
- package/dist/__tests__/browser.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +106 -22
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/list.test.d.ts +2 -0
- package/dist/__tests__/list.test.d.ts.map +1 -0
- package/dist/__tests__/list.test.js +156 -0
- package/dist/__tests__/list.test.js.map +1 -0
- package/dist/__tests__/utilities.test.js +10 -0
- package/dist/__tests__/utilities.test.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +23 -14
- package/dist/browser.js.map +1 -1
- package/dist/dom.d.ts +28 -6
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +69 -54
- package/dist/dom.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +30 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +47 -0
- package/dist/list.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/browser.test.ts +8 -0
- package/src/__tests__/dom.test.ts +114 -23
- package/src/__tests__/list.test.ts +210 -0
- package/src/__tests__/utilities.test.ts +11 -0
- package/src/browser.ts +31 -15
- package/src/dom.ts +79 -51
- package/src/index.ts +1 -0
- package/src/list.ts +77 -0
package/dist/dom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { WindowSize, ScrollPosition, ElementSize, Intersection, Focus, } from "./dom";
|
|
2
2
|
export { MediaQuery, ColorScheme, Mouse, KeyCombo, Idle } from "./browser";
|
|
3
3
|
export { Clipboard, Geolocation, TimeAgo, Pagination, type PaginationOptions, } from "./utilities";
|
|
4
|
+
export { VirtualList, type VirtualItem } from "./list";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,WAAW,EACX,OAAO,EACP,UAAU,EACV,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,WAAW,EACX,OAAO,EACP,UAAU,EACV,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { WindowSize, ScrollPosition, ElementSize, Intersection, Focus, } from "./dom";
|
|
2
2
|
export { MediaQuery, ColorScheme, Mouse, KeyCombo, Idle } from "./browser";
|
|
3
3
|
export { Clipboard, Geolocation, TimeAgo, Pagination, } from "./utilities";
|
|
4
|
+
export { VirtualList } from "./list";
|
|
4
5
|
//# 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,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,WAAW,EACX,OAAO,EACP,UAAU,GAEX,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,WAAW,EACX,OAAO,EACP,UAAU,GAEX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAoB,MAAM,QAAQ,CAAC"}
|
package/dist/list.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Composable } from "@praxisjs/core";
|
|
2
|
+
export interface VirtualItem<T> {
|
|
3
|
+
item: T;
|
|
4
|
+
index: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class VirtualList<T = unknown> extends Composable {
|
|
7
|
+
private readonly containerRef;
|
|
8
|
+
private readonly getItems;
|
|
9
|
+
private readonly itemHeight;
|
|
10
|
+
private readonly buffer;
|
|
11
|
+
visibleItems: Array<VirtualItem<T>>;
|
|
12
|
+
totalHeight: number;
|
|
13
|
+
offsetTop: number;
|
|
14
|
+
offsetBottom: number;
|
|
15
|
+
private readonly _scrollTop;
|
|
16
|
+
private readonly _viewHeight;
|
|
17
|
+
private _scrollCleanup?;
|
|
18
|
+
constructor(containerRef: {
|
|
19
|
+
current: HTMLElement | null;
|
|
20
|
+
}, getItems: () => T[], itemHeight: number, buffer?: number);
|
|
21
|
+
setup(): {
|
|
22
|
+
visibleItems: import("@praxisjs/shared").Computed<VirtualItem<T>[]>;
|
|
23
|
+
totalHeight: import("@praxisjs/shared").Computed<number>;
|
|
24
|
+
offsetTop: import("@praxisjs/shared").Computed<number>;
|
|
25
|
+
offsetBottom: import("@praxisjs/shared").Computed<number>;
|
|
26
|
+
};
|
|
27
|
+
onMount(): void;
|
|
28
|
+
onUnmount(): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5C,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,WAAW,CAAC,CAAC,GAAG,OAAO,CAAE,SAAQ,UAAU;IAWpD,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAbjB,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IAE7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAC3C,OAAO,CAAC,cAAc,CAAC,CAAa;gBAGjB,YAAY,EAAE;QAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;KAAE,EAC7C,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,UAAU,EAAE,MAAM,EAClB,MAAM,SAAI;IAK7B,KAAK;;;;;;IAkCL,OAAO;IAYP,SAAS;CAGV"}
|
package/dist/list.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Composable } from "@praxisjs/core";
|
|
2
|
+
import { computed, signal } from "@praxisjs/core/internal";
|
|
3
|
+
export class VirtualList extends Composable {
|
|
4
|
+
constructor(containerRef, getItems, itemHeight, buffer = 3) {
|
|
5
|
+
super();
|
|
6
|
+
this.containerRef = containerRef;
|
|
7
|
+
this.getItems = getItems;
|
|
8
|
+
this.itemHeight = itemHeight;
|
|
9
|
+
this.buffer = buffer;
|
|
10
|
+
this._scrollTop = signal(0);
|
|
11
|
+
this._viewHeight = signal(600);
|
|
12
|
+
}
|
|
13
|
+
setup() {
|
|
14
|
+
const { _scrollTop, _viewHeight, itemHeight, buffer, getItems } = this;
|
|
15
|
+
const items = computed(getItems);
|
|
16
|
+
const total = computed(() => items().length);
|
|
17
|
+
const startIdx = computed(() => Math.max(0, Math.floor(_scrollTop() / itemHeight) - buffer));
|
|
18
|
+
const endIdx = computed(() => Math.min(total() - 1, Math.ceil((_scrollTop() + _viewHeight()) / itemHeight) + buffer));
|
|
19
|
+
const visibleItems = computed(() => {
|
|
20
|
+
const arr = items();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (let i = startIdx(); i <= endIdx(); i++) {
|
|
23
|
+
result.push({ item: arr[i], index: i });
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
});
|
|
27
|
+
const totalHeight = computed(() => total() * itemHeight);
|
|
28
|
+
const offsetTop = computed(() => startIdx() * itemHeight);
|
|
29
|
+
const offsetBottom = computed(() => Math.max(0, total() - 1 - endIdx()) * itemHeight);
|
|
30
|
+
return { visibleItems, totalHeight, offsetTop, offsetBottom };
|
|
31
|
+
}
|
|
32
|
+
onMount() {
|
|
33
|
+
const el = this.containerRef.current;
|
|
34
|
+
if (!el)
|
|
35
|
+
return;
|
|
36
|
+
const h = el.clientHeight;
|
|
37
|
+
if (h > 0)
|
|
38
|
+
this._viewHeight.set(h);
|
|
39
|
+
const handler = () => { this._scrollTop.set(el.scrollTop); };
|
|
40
|
+
el.addEventListener("scroll", handler);
|
|
41
|
+
this._scrollCleanup = () => { el.removeEventListener("scroll", handler); };
|
|
42
|
+
}
|
|
43
|
+
onUnmount() {
|
|
44
|
+
this._scrollCleanup?.();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=list.js.map
|
package/dist/list.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/composables",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"typescript": "^5.9.3"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@praxisjs/core": "1.
|
|
17
|
+
"@praxisjs/core": "1.5.0",
|
|
18
18
|
"@praxisjs/shared": "0.2.0"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
@@ -283,6 +283,14 @@ describe("KeyCombo (additional)", () => {
|
|
|
283
283
|
expect(pressed()).toBe(true);
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
it("modifier-only combo (ctrl+shift) — keyup resets pressed to false", () => {
|
|
287
|
+
const kc = new KeyCombo("ctrl+shift");
|
|
288
|
+
const { pressed } = kc.setup() as { pressed: () => boolean };
|
|
289
|
+
// Fire keyup to cover the _keyupHandler arrow function inside the !key branch
|
|
290
|
+
window.dispatchEvent(new KeyboardEvent("keyup"));
|
|
291
|
+
expect(pressed()).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
286
294
|
it("modifier-only combo (ctrl+shift) never fires", () => {
|
|
287
295
|
const kc = new KeyCombo("ctrl+shift");
|
|
288
296
|
const { pressed } = kc.setup() as { pressed: () => boolean };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
3
|
|
|
4
|
-
import { signal } from "@praxisjs/core/internal";
|
|
5
|
-
|
|
6
4
|
import { WindowSize, ScrollPosition, ElementSize, Intersection, Focus } from "../dom";
|
|
7
5
|
|
|
8
6
|
// ── WindowSize ────────────────────────────────────────────────────────────────
|
|
@@ -55,6 +53,7 @@ describe("ScrollPosition", () => {
|
|
|
55
53
|
it("updates on scroll event for window", () => {
|
|
56
54
|
const sp = new ScrollPosition(window);
|
|
57
55
|
const { x, y } = sp.setup() as { x: () => number; y: () => number };
|
|
56
|
+
sp.onMount();
|
|
58
57
|
|
|
59
58
|
Object.defineProperty(window, "scrollX", { value: 100, configurable: true });
|
|
60
59
|
Object.defineProperty(window, "scrollY", { value: 200, configurable: true });
|
|
@@ -68,6 +67,7 @@ describe("ScrollPosition", () => {
|
|
|
68
67
|
const el = document.createElement("div");
|
|
69
68
|
const sp = new ScrollPosition(el);
|
|
70
69
|
const { x, y } = sp.setup() as { x: () => number; y: () => number };
|
|
70
|
+
sp.onMount();
|
|
71
71
|
|
|
72
72
|
Object.defineProperty(el, "scrollLeft", { value: 50, configurable: true });
|
|
73
73
|
Object.defineProperty(el, "scrollTop", { value: 75, configurable: true });
|
|
@@ -82,6 +82,7 @@ describe("ScrollPosition", () => {
|
|
|
82
82
|
const remove = vi.spyOn(el, "removeEventListener");
|
|
83
83
|
const sp = new ScrollPosition(el);
|
|
84
84
|
sp.setup();
|
|
85
|
+
sp.onMount();
|
|
85
86
|
sp.onUnmount();
|
|
86
87
|
expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
|
|
87
88
|
});
|
|
@@ -137,6 +138,13 @@ describe("ElementSize", () => {
|
|
|
137
138
|
es.onUnmount();
|
|
138
139
|
expect(disconnect).toHaveBeenCalled();
|
|
139
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
|
+
});
|
|
140
148
|
});
|
|
141
149
|
|
|
142
150
|
// ── Intersection ──────────────────────────────────────────────────────────────
|
|
@@ -187,6 +195,38 @@ describe("Intersection", () => {
|
|
|
187
195
|
int.onUnmount();
|
|
188
196
|
expect(disconnect).toHaveBeenCalled();
|
|
189
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
|
+
});
|
|
190
230
|
});
|
|
191
231
|
|
|
192
232
|
// ── ElementSize (additional) ──────────────────────────────────────────────────
|
|
@@ -205,6 +245,7 @@ describe("ElementSize (additional)", () => {
|
|
|
205
245
|
const ref = { current: el1 as HTMLElement | null };
|
|
206
246
|
const es = new ElementSize(ref);
|
|
207
247
|
es.setup();
|
|
248
|
+
es.onMount();
|
|
208
249
|
|
|
209
250
|
// Disconnect old observer and create new one for el2
|
|
210
251
|
es.onUnmount();
|
|
@@ -214,6 +255,7 @@ describe("ElementSize (additional)", () => {
|
|
|
214
255
|
ref.current = el2;
|
|
215
256
|
const es2 = new ElementSize(ref);
|
|
216
257
|
es2.setup();
|
|
258
|
+
es2.onMount();
|
|
217
259
|
expect(observeFn).toHaveBeenCalledWith(el2);
|
|
218
260
|
});
|
|
219
261
|
});
|
|
@@ -251,15 +293,56 @@ describe("Intersection (additional)", () => {
|
|
|
251
293
|
});
|
|
252
294
|
});
|
|
253
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
|
+
|
|
254
336
|
// ── ScrollPosition (additional) ───────────────────────────────────────────────
|
|
255
337
|
|
|
256
338
|
describe("ScrollPosition (additional)", () => {
|
|
257
|
-
it("
|
|
339
|
+
it("onMount() called twice registers only one scroll listener", () => {
|
|
258
340
|
const el = document.createElement("div");
|
|
259
341
|
const add = vi.spyOn(el, "addEventListener");
|
|
260
342
|
const sp = new ScrollPosition(el);
|
|
261
343
|
sp.setup();
|
|
262
|
-
sp.
|
|
344
|
+
sp.onMount();
|
|
345
|
+
sp.onMount(); // second call must be a no-op
|
|
263
346
|
const scrollCalls = add.mock.calls.filter((c) => c[0] === "scroll").length;
|
|
264
347
|
expect(scrollCalls).toBe(1);
|
|
265
348
|
});
|
|
@@ -282,6 +365,7 @@ describe("Focus", () => {
|
|
|
282
365
|
const ref = { current: el };
|
|
283
366
|
const focus = new Focus(ref);
|
|
284
367
|
const { focused } = focus.setup() as { focused: () => boolean };
|
|
368
|
+
focus.onMount();
|
|
285
369
|
|
|
286
370
|
el.dispatchEvent(new Event("focus"));
|
|
287
371
|
expect(focused()).toBe(true);
|
|
@@ -295,6 +379,7 @@ describe("Focus", () => {
|
|
|
295
379
|
const ref = { current: el };
|
|
296
380
|
const focus = new Focus(ref);
|
|
297
381
|
const { focused } = focus.setup() as { focused: () => boolean };
|
|
382
|
+
focus.onMount();
|
|
298
383
|
|
|
299
384
|
el.dispatchEvent(new Event("focus"));
|
|
300
385
|
el.dispatchEvent(new Event("blur"));
|
|
@@ -309,40 +394,46 @@ describe("Focus", () => {
|
|
|
309
394
|
expect(() => focus.setup()).not.toThrow();
|
|
310
395
|
});
|
|
311
396
|
|
|
312
|
-
it("
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const ref = { get current() { return refSignal(); } };
|
|
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
|
+
});
|
|
319
403
|
|
|
404
|
+
it("onUnmount() with null ref is a no-op", () => {
|
|
405
|
+
const ref = { current: null };
|
|
320
406
|
const focus = new Focus(ref);
|
|
321
407
|
focus.setup();
|
|
408
|
+
expect(() => focus.onUnmount()).not.toThrow();
|
|
409
|
+
});
|
|
322
410
|
|
|
323
|
-
|
|
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();
|
|
324
418
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const el2 = document.createElement("input");
|
|
328
|
-
document.body.appendChild(el2);
|
|
329
|
-
refSignal.set(el2);
|
|
419
|
+
const remove = vi.spyOn(el, "removeEventListener");
|
|
420
|
+
focus.onUnmount();
|
|
330
421
|
|
|
331
|
-
expect(
|
|
332
|
-
expect(
|
|
422
|
+
expect(remove).toHaveBeenCalledWith("focus", expect.any(Function));
|
|
423
|
+
expect(remove).toHaveBeenCalledWith("blur", expect.any(Function));
|
|
333
424
|
|
|
334
|
-
document.body.removeChild(
|
|
335
|
-
document.body.removeChild(el2);
|
|
425
|
+
document.body.removeChild(el);
|
|
336
426
|
});
|
|
337
427
|
|
|
338
|
-
it("
|
|
428
|
+
it("onMount() called twice registers only one listener pair", () => {
|
|
339
429
|
const el = document.createElement("input");
|
|
340
430
|
document.body.appendChild(el);
|
|
341
431
|
const add = vi.spyOn(el, "addEventListener");
|
|
342
432
|
const ref = { current: el };
|
|
343
433
|
const focus = new Focus(ref);
|
|
344
434
|
focus.setup();
|
|
345
|
-
focus.
|
|
435
|
+
focus.onMount();
|
|
436
|
+
focus.onMount(); // second call must be a no-op
|
|
346
437
|
const focusCalls = add.mock.calls.filter((c) => c[0] === "focus").length;
|
|
347
438
|
expect(focusCalls).toBe(1);
|
|
348
439
|
document.body.removeChild(el);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
|
+
|
|
6
|
+
// Import from index to cover re-exports
|
|
7
|
+
import { VirtualList, type VirtualItem } from "../index";
|
|
8
|
+
|
|
9
|
+
type AnyItem = { id: number };
|
|
10
|
+
|
|
11
|
+
function makeItems(count: number): AnyItem[] {
|
|
12
|
+
return Array.from({ length: count }, (_, i) => ({ id: i }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── VirtualList ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("VirtualList", () => {
|
|
18
|
+
const ITEM_HEIGHT = 50;
|
|
19
|
+
const VIEW_HEIGHT = 600;
|
|
20
|
+
const BUFFER = 3;
|
|
21
|
+
|
|
22
|
+
it("setup() returns computed signals for all four properties", () => {
|
|
23
|
+
const ref = { current: null };
|
|
24
|
+
const vl = new VirtualList(ref, () => makeItems(10), ITEM_HEIGHT);
|
|
25
|
+
const view = vl.setup() as {
|
|
26
|
+
visibleItems: () => VirtualItem<AnyItem>[];
|
|
27
|
+
totalHeight: () => number;
|
|
28
|
+
offsetTop: () => number;
|
|
29
|
+
offsetBottom: () => number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(typeof view.visibleItems).toBe("function");
|
|
33
|
+
expect(typeof view.totalHeight).toBe("function");
|
|
34
|
+
expect(typeof view.offsetTop).toBe("function");
|
|
35
|
+
expect(typeof view.offsetBottom).toBe("function");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("totalHeight = items.length * itemHeight", () => {
|
|
39
|
+
const ref = { current: null };
|
|
40
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT);
|
|
41
|
+
const { totalHeight } = vl.setup() as { totalHeight: () => number };
|
|
42
|
+
expect(totalHeight()).toBe(100 * ITEM_HEIGHT);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("offsetTop = 0 when scrolled to top", () => {
|
|
46
|
+
const ref = { current: null };
|
|
47
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT);
|
|
48
|
+
const { offsetTop } = vl.setup() as { offsetTop: () => number };
|
|
49
|
+
expect(offsetTop()).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("visibleItems starts at index 0 with buffer window", () => {
|
|
53
|
+
const ref = { current: null };
|
|
54
|
+
// default viewHeight=600, buffer=3, scrollTop=0
|
|
55
|
+
// startIdx = max(0, floor(0/50) - 3) = 0
|
|
56
|
+
// endIdx = min(99, ceil((0+600)/50) + 3) = min(99, 12+3) = 15
|
|
57
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT, BUFFER);
|
|
58
|
+
const { visibleItems } = vl.setup() as { visibleItems: () => VirtualItem<AnyItem>[] };
|
|
59
|
+
|
|
60
|
+
const items = visibleItems();
|
|
61
|
+
expect(items[0].index).toBe(0);
|
|
62
|
+
expect(items[items.length - 1].index).toBe(15);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("visibleItems contains the correct item references", () => {
|
|
66
|
+
const source = makeItems(20);
|
|
67
|
+
const ref = { current: null };
|
|
68
|
+
const vl = new VirtualList(ref, () => source, ITEM_HEIGHT, 0);
|
|
69
|
+
const { visibleItems } = vl.setup() as { visibleItems: () => VirtualItem<AnyItem>[] };
|
|
70
|
+
|
|
71
|
+
const items = visibleItems();
|
|
72
|
+
expect(items[0].item).toBe(source[0]);
|
|
73
|
+
expect(items[0].index).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("offsetBottom reflects the hidden rows below the window", () => {
|
|
77
|
+
const ref = { current: null };
|
|
78
|
+
// startIdx=0, endIdx=15, total=100
|
|
79
|
+
// offsetBottom = max(0, 100-1-15) * 50 = 84 * 50 = 4200
|
|
80
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT, BUFFER);
|
|
81
|
+
const { offsetBottom } = vl.setup() as { offsetBottom: () => number };
|
|
82
|
+
expect(offsetBottom()).toBe((100 - 1 - 15) * ITEM_HEIGHT);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("empty list: all offsets are 0 and visibleItems is empty", () => {
|
|
86
|
+
const ref = { current: null };
|
|
87
|
+
const vl = new VirtualList(ref, () => [], ITEM_HEIGHT);
|
|
88
|
+
const { visibleItems, totalHeight, offsetTop, offsetBottom } = vl.setup() as {
|
|
89
|
+
visibleItems: () => VirtualItem<AnyItem>[];
|
|
90
|
+
totalHeight: () => number;
|
|
91
|
+
offsetTop: () => number;
|
|
92
|
+
offsetBottom: () => number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(visibleItems()).toHaveLength(0);
|
|
96
|
+
expect(totalHeight()).toBe(0);
|
|
97
|
+
expect(offsetTop()).toBe(0);
|
|
98
|
+
expect(offsetBottom()).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── VirtualList — onMount ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe("VirtualList — onMount", () => {
|
|
105
|
+
const ITEM_HEIGHT = 50;
|
|
106
|
+
|
|
107
|
+
it("onMount() with null ref is a no-op", () => {
|
|
108
|
+
const ref = { current: null };
|
|
109
|
+
const vl = new VirtualList(ref, () => makeItems(10), ITEM_HEIGHT);
|
|
110
|
+
vl.setup();
|
|
111
|
+
expect(() => vl.onMount()).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("onMount() sets viewHeight from container's clientHeight when > 0", () => {
|
|
115
|
+
const el = document.createElement("div");
|
|
116
|
+
Object.defineProperty(el, "clientHeight", { value: 400, configurable: true });
|
|
117
|
+
|
|
118
|
+
const ref = { current: el };
|
|
119
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT, 0);
|
|
120
|
+
const { visibleItems } = vl.setup() as { visibleItems: () => VirtualItem<AnyItem>[] };
|
|
121
|
+
vl.onMount();
|
|
122
|
+
|
|
123
|
+
// With viewHeight=400 and no buffer: endIdx = min(99, ceil(400/50)-1) = 7
|
|
124
|
+
// Visible range [0..7] = 8 items
|
|
125
|
+
const items = visibleItems();
|
|
126
|
+
expect(items.length).toBeLessThan(100);
|
|
127
|
+
expect(items.length).toBeGreaterThan(0);
|
|
128
|
+
// Specifically: ceil((0+400)/50) + 0 = 8; min(99, 8) = 8 → indices 0..8 = 9 items
|
|
129
|
+
expect(items[items.length - 1].index).toBe(8);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("onMount() does not override default viewHeight when clientHeight is 0", () => {
|
|
133
|
+
const el = document.createElement("div");
|
|
134
|
+
Object.defineProperty(el, "clientHeight", { value: 0, configurable: true });
|
|
135
|
+
|
|
136
|
+
const ref = { current: el };
|
|
137
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT, 0);
|
|
138
|
+
const { visibleItems } = vl.setup() as { visibleItems: () => VirtualItem<AnyItem>[] };
|
|
139
|
+
vl.onMount();
|
|
140
|
+
|
|
141
|
+
// Default viewHeight=600 preserved; endIdx = min(99, ceil(600/50)) = 12
|
|
142
|
+
const items = visibleItems();
|
|
143
|
+
expect(items[items.length - 1].index).toBe(12);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("onMount() registers a scroll listener that updates the window", () => {
|
|
147
|
+
const el = document.createElement("div");
|
|
148
|
+
Object.defineProperty(el, "clientHeight", { value: 0, configurable: true });
|
|
149
|
+
|
|
150
|
+
const ref = { current: el };
|
|
151
|
+
const vl = new VirtualList(ref, () => makeItems(100), ITEM_HEIGHT, 0);
|
|
152
|
+
const { offsetTop } = vl.setup() as { offsetTop: () => number };
|
|
153
|
+
vl.onMount();
|
|
154
|
+
|
|
155
|
+
expect(offsetTop()).toBe(0);
|
|
156
|
+
|
|
157
|
+
Object.defineProperty(el, "scrollTop", { value: 500, configurable: true });
|
|
158
|
+
el.dispatchEvent(new Event("scroll"));
|
|
159
|
+
|
|
160
|
+
// scrollTop=500, buffer=0: startIdx = floor(500/50) = 10; offsetTop = 10*50 = 500
|
|
161
|
+
expect(offsetTop()).toBe(500);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── VirtualList — onUnmount ───────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("VirtualList — onUnmount", () => {
|
|
168
|
+
const ITEM_HEIGHT = 50;
|
|
169
|
+
|
|
170
|
+
it("onUnmount() removes the scroll listener", () => {
|
|
171
|
+
const el = document.createElement("div");
|
|
172
|
+
const remove = vi.spyOn(el, "removeEventListener");
|
|
173
|
+
|
|
174
|
+
const ref = { current: el };
|
|
175
|
+
const vl = new VirtualList(ref, () => makeItems(50), ITEM_HEIGHT);
|
|
176
|
+
vl.setup();
|
|
177
|
+
vl.onMount();
|
|
178
|
+
vl.onUnmount();
|
|
179
|
+
|
|
180
|
+
expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("onUnmount() without prior onMount() does not throw", () => {
|
|
184
|
+
const ref = { current: null };
|
|
185
|
+
const vl = new VirtualList(ref, () => makeItems(10), ITEM_HEIGHT);
|
|
186
|
+
vl.setup();
|
|
187
|
+
expect(() => vl.onUnmount()).not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── VirtualList — reactive getItems ──────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe("VirtualList — reactive getItems", () => {
|
|
194
|
+
it("visibleItems updates when the source signal changes", () => {
|
|
195
|
+
const itemsSignal = signal(makeItems(5));
|
|
196
|
+
const ref = { current: null };
|
|
197
|
+
const vl = new VirtualList(ref, () => itemsSignal(), 50);
|
|
198
|
+
const { visibleItems, totalHeight } = vl.setup() as {
|
|
199
|
+
visibleItems: () => VirtualItem<AnyItem>[];
|
|
200
|
+
totalHeight: () => number;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
expect(totalHeight()).toBe(250);
|
|
204
|
+
expect(visibleItems()).toHaveLength(5);
|
|
205
|
+
|
|
206
|
+
itemsSignal.set(makeItems(10));
|
|
207
|
+
expect(totalHeight()).toBe(500);
|
|
208
|
+
expect(visibleItems()).toHaveLength(10);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -213,6 +213,17 @@ describe("TimeAgo", () => {
|
|
|
213
213
|
expect(clearInterval).toHaveBeenCalled();
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
it("interval callback fires after 60s — tick updates and value recomputes", () => {
|
|
217
|
+
const ta = new TimeAgo(() => now - 30_000, "en");
|
|
218
|
+
const { value } = ta.setup() as { value: () => string };
|
|
219
|
+
const before = value();
|
|
220
|
+
vi.advanceTimersByTime(60_000);
|
|
221
|
+
// After 60s the tick signal updates; value() still produces a valid string
|
|
222
|
+
expect(typeof value()).toBe("string");
|
|
223
|
+
expect(value()).not.toBe(undefined);
|
|
224
|
+
void before;
|
|
225
|
+
});
|
|
226
|
+
|
|
216
227
|
it("future date produces a positive relative time string (e.g. 'in 30 seconds')", () => {
|
|
217
228
|
// A future timestamp produces a positive diff; Intl.RelativeTimeFormat formats it as "in X"
|
|
218
229
|
const ta = new TimeAgo(() => now + 30_000, "en");
|