mtrl-addons 0.1.2 → 0.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 (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -0,0 +1,352 @@
1
+ // src/styles/components/_vlist.scss
2
+ @use "../../../../mtrl/src/styles/abstract/base" as base;
3
+ @use "../../../../mtrl/src/styles/abstract/variables" as v;
4
+ @use "../../../../mtrl/src/styles/abstract/functions" as f;
5
+ @use "../../../../mtrl/src/styles/abstract/mixins" as m;
6
+ @use "../../../../mtrl/src/styles/abstract/theme" as t;
7
+
8
+ // ===========================
9
+ // Variables & Configuration
10
+ // ===========================
11
+
12
+ // Component prefixes
13
+ $prefix: base.$prefix;
14
+ $component: "#{$prefix}-vlist";
15
+ $viewport: "#{$prefix}-viewport";
16
+ $viewport-item: "#{$prefix}-viewport-item";
17
+
18
+ // Transitions
19
+ $transition-duration: v.motion("duration-medium1");
20
+ $transition-easing: v.motion("easing-standard");
21
+
22
+ // Common transitions
23
+ $color-transition: color $transition-duration $transition-easing;
24
+ $bg-transition: background-color $transition-duration $transition-easing;
25
+ $opacity-transition: opacity $transition-duration $transition-easing;
26
+
27
+ // ===========================
28
+ // Animations
29
+ // ===========================
30
+
31
+ @keyframes placeholder-pulse {
32
+ 0%,
33
+ 100% {
34
+ opacity: 0.6;
35
+ }
36
+ 50% {
37
+ opacity: 0.4;
38
+ }
39
+ }
40
+
41
+ @keyframes fade-in {
42
+ from {
43
+ opacity: 0.6;
44
+ }
45
+ to {
46
+ opacity: 1;
47
+ }
48
+ }
49
+
50
+ @keyframes item-updated {
51
+ 0% {
52
+ background-color: t.alpha("primary", 0.25);
53
+ box-shadow: inset 0 0 0 2px t.alpha("primary", 0.4);
54
+ }
55
+ 100% {
56
+ background-color: transparent;
57
+ box-shadow: inset 0 0 0 2px transparent;
58
+ }
59
+ }
60
+
61
+ // ===========================
62
+ // Main VList Container
63
+ // ===========================
64
+
65
+ .#{$component} {
66
+ position: relative;
67
+ width: 100%;
68
+ height: 100%;
69
+ min-height: 100px;
70
+ background-color: t.color("surface");
71
+ border: 2px solid var(--mtrl-sys-color-outline-variant);
72
+ border-radius: 3px;
73
+ transition: $bg-transition;
74
+
75
+ // Performance optimizations
76
+ contain: layout style paint;
77
+ transform: translateZ(0);
78
+ backface-visibility: hidden;
79
+
80
+ // Selection mode
81
+ &--selection {
82
+ cursor: pointer;
83
+ .#{$viewport-item}:hover {
84
+ background-color: t.alpha("on-surface-variant", 0.1);
85
+ }
86
+ // Selected state
87
+ .#{$viewport-item}--selected,
88
+ .#{$viewport-item}--selected:hover {
89
+ background-color: t.color("secondary-container");
90
+ color: t.color("on-secondary-container");
91
+ transition: $bg-transition, $color-transition;
92
+
93
+ // // Update state layer color for selected state
94
+ // &::before {
95
+ // background-color: t.color("on-secondary-container");
96
+ // }
97
+
98
+ // Update text and icon colors for selected state
99
+ .#{$component}-item {
100
+ &-leading,
101
+ &-trailing,
102
+ &-supporting,
103
+ &-overline,
104
+ &-meta {
105
+ color: t.color("on-secondary-container");
106
+ transition: $color-transition;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // Disabled state
113
+ &--disabled {
114
+ pointer-events: none;
115
+ opacity: 0.38;
116
+ transition: $opacity-transition;
117
+ }
118
+ }
119
+
120
+ // ===========================
121
+ // Viewport Structure
122
+ // ===========================
123
+
124
+ .#{$viewport} {
125
+ position: relative;
126
+ width: 100%;
127
+ height: 100%;
128
+ overflow: hidden;
129
+
130
+ // Items container
131
+ &-items {
132
+ position: relative;
133
+ width: 100%;
134
+ height: 100%;
135
+ padding: 8px 0;
136
+ will-change: transform;
137
+ }
138
+ }
139
+
140
+ .#{$viewport-item} {
141
+ user-select: none;
142
+ opacity: 1;
143
+ transition: $opacity-transition;
144
+ will-change: transform;
145
+ // Apply fade-in animation to items that replace placeholders
146
+ &--replaced {
147
+ animation: fade-in 0.3s ease-out;
148
+ }
149
+ // Apply highlight animation when item is updated
150
+ &--updated,
151
+ &--updated > * {
152
+ animation: item-updated 0.6s ease-out;
153
+ }
154
+
155
+ // Inner item update animation (applied to template's root element)
156
+ .item--updated {
157
+ animation: item-updated 0.6s ease-out;
158
+ }
159
+ }
160
+
161
+ .#{$viewport-item} {
162
+ padding: 11px 12px;
163
+ display: flex;
164
+ align-items: start;
165
+ transition: background-color 0.2s ease;
166
+ // align-items: center;
167
+ min-height: 48px;
168
+ left: 0;
169
+ right: 0;
170
+ width: 100%;
171
+ will-change: transform;
172
+ contain: layout style;
173
+ gap: 16px;
174
+ color: var(--mtrl-sys-color-on-surface);
175
+ overflow: hidden;
176
+ }
177
+
178
+ .#{$viewport-item}__avatar,
179
+ .#{$viewport-item}__image {
180
+ width: 40px;
181
+ height: 40px;
182
+ margin-top: 4px;
183
+ border-radius: 50%;
184
+ background-color: var(--mtrl-sys-color-primary-container);
185
+ color: white;
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ font-weight: bold;
190
+ flex-shrink: 0;
191
+ }
192
+
193
+ .#{$viewport-item}__details {
194
+ flex: 1;
195
+ min-width: 0;
196
+ margin-left: 12px;
197
+ }
198
+
199
+ .#{$viewport-item}__headline {
200
+ font-weight: 500;
201
+ }
202
+
203
+ .#{$viewport-item}__text {
204
+ // color: #666;
205
+ font-size: 14px;
206
+ white-space: nowrap;
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ }
210
+
211
+ .#{$viewport-item}__meta {
212
+ color: var(--mtrl-sys-color-on-surface-variant);
213
+ font-size: 12px;
214
+ margin-top: 2px;
215
+ }
216
+
217
+ // ===========================
218
+ // Placeholder Styling
219
+ // ===========================
220
+
221
+ .#{$viewport-item}--placeholder {
222
+ opacity: 0.6; // Match the animation start/end opacity
223
+ animation: placeholder-pulse 2s ease-in-out infinite;
224
+
225
+ // Placeholder content blocks - style any text elements within
226
+ .#{$viewport-item}__headline,
227
+ .#{$viewport-item}__text,
228
+ .#{$viewport-item}__meta {
229
+ position: relative;
230
+ display: inline-block;
231
+ font-size: 0.8em;
232
+ color: transparent;
233
+ background-color: var(--mtrl-sys-color-on-surface);
234
+ border-radius: 0.1em;
235
+ opacity: 0.4;
236
+ text-decoration: none;
237
+ line-height: 1;
238
+ padding: 0 0 0.05em;
239
+ vertical-align: middle;
240
+ }
241
+
242
+ // Layout adjustments
243
+ .#{$viewport-item}__text,
244
+ .#{$viewport-item}__meta {
245
+ margin-top: 0.2em;
246
+ }
247
+
248
+ .#{$viewport-item}__meta {
249
+ font-size: 10px;
250
+ }
251
+
252
+ // User-specific placeholder elements
253
+ .#{$viewport-item}__user-headline,
254
+ .#{$viewport-item}__user-text {
255
+ text-transform: capitalize;
256
+ }
257
+
258
+ // Avatar placeholder
259
+ .#{$viewport-item}__avatar {
260
+ background-color: var(--mtrl-sys-color-primary-container);
261
+ color: var(--mtrl-sys-color-primary-container);
262
+ opacity: 1;
263
+ }
264
+ }
265
+
266
+ // ===========================
267
+ // Custom Scrollbar
268
+ // ===========================
269
+
270
+ .#{$viewport}__scrollbar {
271
+ position: absolute;
272
+ top: 0;
273
+ right: 0;
274
+ width: 8px;
275
+ height: 100%;
276
+ padding: 0;
277
+ opacity: 0;
278
+ transition: opacity 0.3s ease;
279
+ cursor: pointer;
280
+ z-index: 10;
281
+
282
+ // Visibility states
283
+ &--visible,
284
+ &--dragging,
285
+ &:hover {
286
+ opacity: 1;
287
+ }
288
+
289
+ &:hover {
290
+ background: rgba(0, 0, 0, 0.05);
291
+ }
292
+
293
+ // Scrollbar thumb
294
+ &-thumb {
295
+ position: absolute;
296
+ top: 0;
297
+ width: 6px;
298
+ padding: 1px;
299
+ background: rgba(0, 0, 0, 0.3);
300
+ border-radius: 4px;
301
+ will-change: transform;
302
+ cursor: grab;
303
+ transition: background 0.2s ease;
304
+
305
+ &:hover {
306
+ background: rgba(0, 0, 0, 0.5);
307
+ }
308
+
309
+ &:active,
310
+ &--dragging {
311
+ cursor: grabbing;
312
+ background: rgba(0, 0, 0, 0.6);
313
+ transition: none;
314
+ }
315
+ }
316
+ }
317
+
318
+ // ===========================
319
+ // Dark Theme Support
320
+ // ===========================
321
+
322
+ @media (prefers-color-scheme: dark) {
323
+ .#{$viewport}__scrollbar {
324
+ background: transparent;
325
+
326
+ &-thumb {
327
+ background: rgba(255, 255, 255, 0.4);
328
+
329
+ &:hover {
330
+ background: rgba(255, 255, 255, 0.6);
331
+ }
332
+
333
+ &:active,
334
+ &--dragging {
335
+ background: rgba(255, 255, 255, 0.4);
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ // ===========================
342
+ // Performance Optimizations
343
+ // ===========================
344
+
345
+ // High-density displays
346
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
347
+ .#{$viewport-item} {
348
+ text-rendering: optimizeLegibility;
349
+ -webkit-font-smoothing: antialiased;
350
+ -moz-osx-font-smoothing: grayscale;
351
+ }
352
+ }
@@ -5,7 +5,7 @@
5
5
  // These styles only add truly addons-specific features that don't exist in mtrl base.
6
6
 
7
7
  // Import component styles (only addons-specific enhancements)
8
- @forward "./components/list";
8
+ @forward "./components/vlist";
9
9
 
10
10
  // Future addons-specific component styles will be added here
11
11
  // @forward "./components/datagrid"; // Advanced grid functionality
@@ -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
+ });