mtrl-addons 0.1.2 → 0.2.1

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 (115) hide show
  1. package/build.js +139 -86
  2. package/package.json +13 -4
  3. package/scripts/debug/vlist-selection.ts +121 -0
  4. package/src/components/index.ts +5 -41
  5. package/src/components/{list → vlist}/config.ts +66 -95
  6. package/src/components/vlist/constants.ts +23 -0
  7. package/src/components/vlist/features/api.ts +322 -0
  8. package/src/components/vlist/features/index.ts +10 -0
  9. package/src/components/vlist/features/selection.ts +444 -0
  10. package/src/components/vlist/features/viewport.ts +65 -0
  11. package/src/components/vlist/index.ts +16 -0
  12. package/src/components/{list → vlist}/types.ts +104 -26
  13. package/src/components/vlist/vlist.ts +92 -0
  14. package/src/core/compose/features/gestures/index.ts +227 -0
  15. package/src/core/compose/features/gestures/longpress.ts +383 -0
  16. package/src/core/compose/features/gestures/pan.ts +424 -0
  17. package/src/core/compose/features/gestures/pinch.ts +475 -0
  18. package/src/core/compose/features/gestures/rotate.ts +485 -0
  19. package/src/core/compose/features/gestures/swipe.ts +492 -0
  20. package/src/core/compose/features/gestures/tap.ts +334 -0
  21. package/src/core/compose/features/index.ts +2 -38
  22. package/src/core/compose/index.ts +13 -29
  23. package/src/core/gestures/index.ts +31 -0
  24. package/src/core/gestures/longpress.ts +68 -0
  25. package/src/core/gestures/manager.ts +418 -0
  26. package/src/core/gestures/pan.ts +48 -0
  27. package/src/core/gestures/pinch.ts +58 -0
  28. package/src/core/gestures/rotate.ts +58 -0
  29. package/src/core/gestures/swipe.ts +66 -0
  30. package/src/core/gestures/tap.ts +45 -0
  31. package/src/core/gestures/types.ts +387 -0
  32. package/src/core/gestures/utils.ts +128 -0
  33. package/src/core/index.ts +27 -151
  34. package/src/core/layout/schema.ts +73 -35
  35. package/src/core/layout/types.ts +5 -2
  36. package/src/core/viewport/constants.ts +140 -0
  37. package/src/core/viewport/features/base.ts +73 -0
  38. package/src/core/viewport/features/collection.ts +882 -0
  39. package/src/core/viewport/features/events.ts +130 -0
  40. package/src/core/viewport/features/index.ts +20 -0
  41. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +27 -30
  42. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  43. package/src/core/viewport/features/momentum.ts +260 -0
  44. package/src/core/viewport/features/placeholders.ts +335 -0
  45. package/src/core/viewport/features/rendering.ts +568 -0
  46. package/src/core/viewport/features/scrollbar.ts +434 -0
  47. package/src/core/viewport/features/scrolling.ts +618 -0
  48. package/src/core/viewport/features/utils.ts +88 -0
  49. package/src/core/viewport/features/virtual.ts +384 -0
  50. package/src/core/viewport/index.ts +31 -0
  51. package/src/core/viewport/types.ts +133 -0
  52. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  53. package/src/core/viewport/viewport.ts +246 -0
  54. package/src/index.ts +0 -7
  55. package/src/styles/components/_vlist.scss +331 -0
  56. package/src/styles/index.scss +1 -1
  57. package/test/components/vlist-selection.test.ts +240 -0
  58. package/test/components/vlist.test.ts +63 -0
  59. package/test/core/collection/adapter.test.ts +161 -0
  60. package/bun.lock +0 -792
  61. package/src/components/list/api.ts +0 -314
  62. package/src/components/list/constants.ts +0 -56
  63. package/src/components/list/features/api.ts +0 -428
  64. package/src/components/list/features/index.ts +0 -31
  65. package/src/components/list/features/list-manager.ts +0 -502
  66. package/src/components/list/index.ts +0 -39
  67. package/src/components/list/list.ts +0 -234
  68. package/src/core/collection/base-collection.ts +0 -100
  69. package/src/core/collection/collection-composer.ts +0 -178
  70. package/src/core/collection/collection.ts +0 -745
  71. package/src/core/collection/constants.ts +0 -172
  72. package/src/core/collection/events.ts +0 -428
  73. package/src/core/collection/features/api/loading.ts +0 -279
  74. package/src/core/collection/features/operations/data-operations.ts +0 -147
  75. package/src/core/collection/index.ts +0 -104
  76. package/src/core/collection/state.ts +0 -497
  77. package/src/core/collection/types.ts +0 -404
  78. package/src/core/compose/features/collection.ts +0 -119
  79. package/src/core/compose/features/selection.ts +0 -213
  80. package/src/core/compose/features/styling.ts +0 -108
  81. package/src/core/list-manager/api.ts +0 -599
  82. package/src/core/list-manager/config.ts +0 -593
  83. package/src/core/list-manager/constants.ts +0 -268
  84. package/src/core/list-manager/features/api.ts +0 -58
  85. package/src/core/list-manager/features/collection/collection.ts +0 -705
  86. package/src/core/list-manager/features/collection/index.ts +0 -17
  87. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  88. package/src/core/list-manager/features/viewport/index.ts +0 -16
  89. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  90. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  91. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  92. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  93. package/src/core/list-manager/features/viewport/template.ts +0 -220
  94. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  95. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  96. package/src/core/list-manager/index.ts +0 -279
  97. package/src/core/list-manager/list-manager.ts +0 -206
  98. package/src/core/list-manager/types.ts +0 -439
  99. package/src/core/list-manager/utils/calculations.ts +0 -290
  100. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  101. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  102. package/src/styles/components/_list.scss +0 -244
  103. package/src/types/mtrl.d.ts +0 -6
  104. package/test/components/list.test.ts +0 -256
  105. package/test/core/collection/failed-ranges.test.ts +0 -270
  106. package/test/core/compose/features.test.ts +0 -183
  107. package/test/core/list-manager/features/collection.test.ts +0 -704
  108. package/test/core/list-manager/features/viewport.test.ts +0 -698
  109. package/test/core/list-manager/list-manager.test.ts +0 -593
  110. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  111. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  112. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  113. package/tsconfig.build.json +0 -23
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -0,0 +1,240 @@
1
+ // test/components/vlist-selection.test.ts
2
+
3
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
4
+ import { createVList } from "../../src/components/vlist";
5
+ import type { VListComponent } from "../../src/components/vlist/types";
6
+
7
+ describe("VList Selection", () => {
8
+ let container: HTMLElement;
9
+ let vlist: VListComponent<any>;
10
+
11
+ beforeEach(() => {
12
+ // Create container
13
+ container = document.createElement("div");
14
+ container.style.height = "400px";
15
+ container.style.width = "300px";
16
+ document.body.appendChild(container);
17
+ });
18
+
19
+ afterEach(() => {
20
+ // Cleanup
21
+ if (vlist?.destroy) {
22
+ vlist.destroy();
23
+ }
24
+ document.body.removeChild(container);
25
+ });
26
+
27
+ test("should initialize without selection when not enabled", () => {
28
+ const items = Array.from({ length: 10 }, (_, i) => ({
29
+ id: `item-${i}`,
30
+ name: `Item ${i}`,
31
+ }));
32
+
33
+ vlist = createVList({
34
+ container,
35
+ items,
36
+ template: (item) => {
37
+ const div = document.createElement("div");
38
+ div.className = "list-item";
39
+ div.textContent = item.name;
40
+ return div;
41
+ },
42
+ });
43
+
44
+ // Selection methods should not be available
45
+ expect(vlist.selectItems).toBeUndefined();
46
+ expect(vlist.getSelectedItems).toBeUndefined();
47
+ });
48
+
49
+ test("should initialize with selection when enabled", () => {
50
+ const items = Array.from({ length: 10 }, (_, i) => ({
51
+ id: `item-${i}`,
52
+ name: `Item ${i}`,
53
+ }));
54
+
55
+ vlist = createVList({
56
+ container,
57
+ items,
58
+ selection: {
59
+ enabled: true,
60
+ mode: "single",
61
+ },
62
+ template: (item) => {
63
+ const div = document.createElement("div");
64
+ div.className = "list-item";
65
+ div.textContent = item.name;
66
+ return div;
67
+ },
68
+ });
69
+
70
+ // Selection methods should be available
71
+ expect(vlist.selectItems).toBeDefined();
72
+ expect(vlist.getSelectedItems).toBeDefined();
73
+ expect(vlist.clearSelection).toBeDefined();
74
+ });
75
+
76
+ test("should handle single selection mode", () => {
77
+ const items = Array.from({ length: 10 }, (_, i) => ({
78
+ id: `item-${i}`,
79
+ name: `Item ${i}`,
80
+ }));
81
+
82
+ vlist = createVList({
83
+ container,
84
+ items,
85
+ selection: {
86
+ enabled: true,
87
+ mode: "single",
88
+ },
89
+ template: (item) => {
90
+ const div = document.createElement("div");
91
+ div.className = "list-item";
92
+ div.textContent = item.name;
93
+ return div;
94
+ },
95
+ });
96
+
97
+ // Select first item
98
+ vlist.selectItems([0]);
99
+ expect(vlist.getSelectedIndices()).toEqual([0]);
100
+ expect(vlist.getSelectedItems()).toEqual([items[0]]);
101
+
102
+ // Select another item - should replace selection
103
+ vlist.selectItems([2]);
104
+ expect(vlist.getSelectedIndices()).toEqual([2]);
105
+ expect(vlist.getSelectedItems()).toEqual([items[2]]);
106
+ });
107
+
108
+ test("should handle multiple selection mode", () => {
109
+ const items = Array.from({ length: 10 }, (_, i) => ({
110
+ id: `item-${i}`,
111
+ name: `Item ${i}`,
112
+ }));
113
+
114
+ vlist = createVList({
115
+ container,
116
+ items,
117
+ selection: {
118
+ enabled: true,
119
+ mode: "multiple",
120
+ },
121
+ template: (item) => {
122
+ const div = document.createElement("div");
123
+ div.className = "list-item";
124
+ div.textContent = item.name;
125
+ return div;
126
+ },
127
+ });
128
+
129
+ // Select multiple items
130
+ vlist.selectItems([0, 2, 4]);
131
+ expect(vlist.getSelectedIndices().sort()).toEqual([0, 2, 4]);
132
+
133
+ // Deselect one item
134
+ vlist.deselectItems([2]);
135
+ expect(vlist.getSelectedIndices().sort()).toEqual([0, 4]);
136
+
137
+ // Clear selection
138
+ vlist.clearSelection();
139
+ expect(vlist.getSelectedIndices()).toEqual([]);
140
+ });
141
+
142
+ test("should handle click selection", async () => {
143
+ const items = Array.from({ length: 10 }, (_, i) => ({
144
+ id: `item-${i}`,
145
+ name: `Item ${i}`,
146
+ }));
147
+
148
+ vlist = createVList({
149
+ container,
150
+ items,
151
+ selection: {
152
+ enabled: true,
153
+ mode: "single",
154
+ },
155
+ template: (item) => {
156
+ const div = document.createElement("div");
157
+ div.className = "list-item";
158
+ div.textContent = item.name;
159
+ return div;
160
+ },
161
+ });
162
+
163
+ // Wait for render
164
+ await new Promise((resolve) => setTimeout(resolve, 50));
165
+
166
+ // Find first item element
167
+ const firstItem = container.querySelector(
168
+ '[data-index="0"]'
169
+ ) as HTMLElement;
170
+ expect(firstItem).toBeTruthy();
171
+
172
+ // Click on first item
173
+ firstItem.click();
174
+
175
+ // Check selection
176
+ expect(vlist.getSelectedIndices()).toEqual([0]);
177
+ expect(firstItem.classList.contains("mtrl-list-item--selected")).toBe(true);
178
+ });
179
+
180
+ test("should emit selection events", async () => {
181
+ const items = Array.from({ length: 10 }, (_, i) => ({
182
+ id: `item-${i}`,
183
+ name: `Item ${i}`,
184
+ }));
185
+
186
+ let selectionChangeEvent: any = null;
187
+
188
+ vlist = createVList({
189
+ container,
190
+ items,
191
+ selection: {
192
+ enabled: true,
193
+ mode: "single",
194
+ onSelectionChange: (selectedItems, selectedIndices) => {
195
+ selectionChangeEvent = { selectedItems, selectedIndices };
196
+ },
197
+ },
198
+ template: (item) => {
199
+ const div = document.createElement("div");
200
+ div.className = "list-item";
201
+ div.textContent = item.name;
202
+ return div;
203
+ },
204
+ });
205
+
206
+ // Select an item
207
+ vlist.selectItems([3]);
208
+
209
+ // Check event was fired
210
+ expect(selectionChangeEvent).toBeTruthy();
211
+ expect(selectionChangeEvent.selectedIndices).toEqual([3]);
212
+ expect(selectionChangeEvent.selectedItems).toEqual([items[3]]);
213
+ });
214
+
215
+ test("should handle initial selection", () => {
216
+ const items = Array.from({ length: 10 }, (_, i) => ({
217
+ id: `item-${i}`,
218
+ name: `Item ${i}`,
219
+ }));
220
+
221
+ vlist = createVList({
222
+ container,
223
+ items,
224
+ selection: {
225
+ enabled: true,
226
+ mode: "multiple",
227
+ selectedIndices: [1, 3, 5],
228
+ },
229
+ template: (item) => {
230
+ const div = document.createElement("div");
231
+ div.className = "list-item";
232
+ div.textContent = item.name;
233
+ return div;
234
+ },
235
+ });
236
+
237
+ // Check initial selection
238
+ expect(vlist.getSelectedIndices().sort()).toEqual([1, 3, 5]);
239
+ });
240
+ });
@@ -0,0 +1,63 @@
1
+ // test/components/vlist.test.ts
2
+
3
+ import { describe, it, expect, beforeEach } from "bun:test";
4
+ import { JSDOM } from "jsdom";
5
+
6
+ // Mock DOM environment for testing
7
+ const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
8
+ global.document = dom.window.document;
9
+ global.HTMLElement = dom.window.HTMLElement;
10
+ global.window = dom.window as any;
11
+ global.navigator = dom.window.navigator;
12
+ global.requestAnimationFrame = (cb: FrameRequestCallback) => {
13
+ setTimeout(cb, 0);
14
+ return 0;
15
+ };
16
+
17
+ // Import VList after DOM setup
18
+ import { createVList } from "../../src/components/vlist";
19
+
20
+ describe("VList Component", () => {
21
+ let container: HTMLElement;
22
+
23
+ beforeEach(() => {
24
+ container = document.createElement("div");
25
+ document.body.appendChild(container);
26
+ });
27
+
28
+ it("should create a VList component", () => {
29
+ const vlist = createVList({
30
+ container,
31
+ items: ["Item 1", "Item 2", "Item 3"],
32
+ template: (item) => `<div>${item}</div>`,
33
+ });
34
+
35
+ expect(vlist).toBeDefined();
36
+ expect(vlist.element).toBeDefined();
37
+ expect(vlist.element.tagName).toBe("DIV");
38
+ expect(vlist.element.className).toContain("mtrl-vlist");
39
+ });
40
+
41
+ it("should have viewport functionality", () => {
42
+ const vlist = createVList({
43
+ container,
44
+ items: Array.from({ length: 100 }, (_, i) => `Item ${i}`),
45
+ });
46
+
47
+ expect(vlist.viewport).toBeDefined();
48
+ expect(typeof vlist.viewport.scrollToIndex).toBe("function");
49
+ expect(typeof vlist.viewport.getVisibleRange).toBe("function");
50
+ });
51
+
52
+ it("should have public API methods", () => {
53
+ const vlist = createVList({ container });
54
+
55
+ expect(typeof vlist.setItems).toBe("function");
56
+ expect(typeof vlist.getItems).toBe("function");
57
+ expect(typeof vlist.scrollToIndex).toBe("function");
58
+ expect(typeof vlist.scrollToTop).toBe("function");
59
+ expect(typeof vlist.scrollToBottom).toBe("function");
60
+ expect(typeof vlist.getState).toBe("function");
61
+ expect(typeof vlist.destroy).toBe("function");
62
+ });
63
+ });
@@ -0,0 +1,161 @@
1
+ // test/core/collection/adapter.test.ts
2
+
3
+ import { describe, test, expect, beforeEach } from "bun:test";
4
+ import {
5
+ createRouteAdapter,
6
+ withRouteAdapter,
7
+ } from "../../../src/core/collection/features/adapter";
8
+ import { createCollection } from "../../../src/core/collection";
9
+
10
+ describe("Route Adapter", () => {
11
+ let fetchMock: any;
12
+
13
+ beforeEach(() => {
14
+ // Mock fetch
15
+ fetchMock = {
16
+ calls: [] as any[],
17
+ response: {
18
+ items: [
19
+ { id: "1", name: "Item 1" },
20
+ { id: "2", name: "Item 2" },
21
+ ],
22
+ meta: {
23
+ total: 100,
24
+ hasNext: true,
25
+ cursor: "next-cursor",
26
+ },
27
+ },
28
+ };
29
+
30
+ global.fetch = async (url: any, options: any) => {
31
+ fetchMock.calls.push({ url, options });
32
+ return {
33
+ ok: true,
34
+ headers: {
35
+ get: () => "application/json",
36
+ },
37
+ json: async () => fetchMock.response,
38
+ } as any;
39
+ };
40
+ });
41
+
42
+ describe("createRouteAdapter", () => {
43
+ test("creates adapter with default config", () => {
44
+ const adapter = createRouteAdapter();
45
+ expect(adapter).toBeDefined();
46
+ expect(adapter.read).toBeDefined();
47
+ expect(adapter.disconnect).toBeDefined();
48
+ });
49
+
50
+ test("makes API request with correct parameters", async () => {
51
+ const adapter = createRouteAdapter({
52
+ base: "/api",
53
+ endpoints: { list: "/users" },
54
+ headers: { Authorization: "Bearer token" },
55
+ pagination: { strategy: "offset" }, // Use offset strategy for this test
56
+ });
57
+
58
+ const result = await adapter.read({
59
+ offset: 0,
60
+ limit: 20,
61
+ search: "john",
62
+ sort: "name",
63
+ });
64
+
65
+ expect(fetchMock.calls).toHaveLength(1);
66
+ const [call] = fetchMock.calls;
67
+ expect(call.url).toContain("/api/users");
68
+ expect(call.url).toContain("offset=0");
69
+ expect(call.url).toContain("limit=20");
70
+ expect(call.url).toContain("search=john");
71
+ expect(call.url).toContain("sort=name");
72
+ expect(call.options.headers["Authorization"]).toBe("Bearer token");
73
+ });
74
+
75
+ test("handles different pagination strategies", async () => {
76
+ // Cursor pagination
77
+ const cursorAdapter = createRouteAdapter({
78
+ base: "/api",
79
+ endpoints: { list: "/items" },
80
+ pagination: { strategy: "cursor" },
81
+ });
82
+
83
+ await cursorAdapter.read({ cursor: "abc123", limit: 10 });
84
+ expect(fetchMock.calls[0].url).toContain("cursor=abc123");
85
+
86
+ // Page pagination
87
+ const pageAdapter = createRouteAdapter({
88
+ base: "/api",
89
+ endpoints: { list: "/items" },
90
+ pagination: { strategy: "page" },
91
+ });
92
+
93
+ await pageAdapter.read({ page: 3, limit: 10 });
94
+ expect(fetchMock.calls[1].url).toContain("page=3");
95
+ });
96
+
97
+ test("transforms query operators", async () => {
98
+ const adapter = createRouteAdapter({
99
+ base: "/api",
100
+ endpoints: { list: "/items" },
101
+ });
102
+
103
+ await adapter.read({
104
+ filters: {
105
+ age: { GT: 18, LTE: 65 },
106
+ status: { EQ: "active" },
107
+ tags: { IN: ["premium", "verified"] },
108
+ },
109
+ });
110
+
111
+ const url = fetchMock.calls[0].url;
112
+ expect(url).toContain("age_gt=18");
113
+ expect(url).toContain("age_lte=65");
114
+ expect(url).toContain("status_eq=active");
115
+ expect(url).toContain("tags_in=premium");
116
+ expect(url).toContain("tags_in=verified");
117
+ });
118
+
119
+ test("handles caching", async () => {
120
+ const adapter = createRouteAdapter({
121
+ base: "/api",
122
+ endpoints: { list: "/items" },
123
+ cache: true,
124
+ pagination: { strategy: "offset" }, // Use offset strategy to test offset params
125
+ });
126
+
127
+ // First call
128
+ await adapter.read({ offset: 0, limit: 10 });
129
+ expect(fetchMock.calls).toHaveLength(1);
130
+
131
+ // Second call with same params - should use cache
132
+ await adapter.read({ offset: 0, limit: 10 });
133
+ expect(fetchMock.calls).toHaveLength(1);
134
+
135
+ // Different params - should make new request
136
+ await adapter.read({ offset: 10, limit: 10 });
137
+ expect(fetchMock.calls).toHaveLength(2);
138
+ });
139
+ });
140
+
141
+ describe("withRouteAdapter", () => {
142
+ test("adds route adapter to collection", async () => {
143
+ const collection = createCollection({
144
+ pageSize: 20,
145
+ });
146
+
147
+ const enhancedCollection = withRouteAdapter({
148
+ base: "/api",
149
+ endpoints: { list: "/users" },
150
+ })(collection);
151
+
152
+ expect(enhancedCollection.disconnect).toBeDefined();
153
+
154
+ // Load data
155
+ await enhancedCollection.loadPage(1);
156
+
157
+ expect(fetchMock.calls).toHaveLength(1);
158
+ expect(fetchMock.calls[0].url).toContain("/api/users");
159
+ });
160
+ });
161
+ });