ripple 0.2.171 → 0.2.173
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/package.json +2 -2
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/bindings.js +30 -18
- package/src/runtime/internal/client/events.js +30 -3
- package/src/runtime/internal/client/render.js +1 -1
- package/tests/client/events.test.ripple +562 -0
- package/tests/client/input-value.test.ripple +297 -0
- package/types/index.d.ts +2 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.173",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
"typescript": "^5.9.2"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"ripple": "0.2.
|
|
84
|
+
"ripple": "0.2.173"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -311,32 +311,28 @@ export function bindGroup(maybe_tracked) {
|
|
|
311
311
|
return (input) => {
|
|
312
312
|
var is_checkbox = input.getAttribute('type') === 'checkbox';
|
|
313
313
|
|
|
314
|
-
// Store the input's value
|
|
315
|
-
// @ts-ignore
|
|
316
|
-
input.__value = input.value;
|
|
317
|
-
|
|
318
314
|
var clear_event = on(input, 'change', () => {
|
|
319
|
-
|
|
320
|
-
var
|
|
315
|
+
var value = input.value;
|
|
316
|
+
var result;
|
|
321
317
|
|
|
322
318
|
if (is_checkbox) {
|
|
323
319
|
/** @type {Array<any>} */
|
|
324
|
-
var
|
|
320
|
+
var list = get(tracked) || [];
|
|
325
321
|
|
|
326
322
|
if (input.checked) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
value = [...current_value, value];
|
|
323
|
+
if (!list.includes(value)) {
|
|
324
|
+
result = [...list, value];
|
|
330
325
|
} else {
|
|
331
|
-
|
|
326
|
+
result = list;
|
|
332
327
|
}
|
|
333
328
|
} else {
|
|
334
|
-
|
|
335
|
-
value = current_value.filter((v) => v !== value);
|
|
329
|
+
result = list.filter((v) => v !== value);
|
|
336
330
|
}
|
|
331
|
+
} else {
|
|
332
|
+
result = input.value;
|
|
337
333
|
}
|
|
338
334
|
|
|
339
|
-
set(tracked,
|
|
335
|
+
set(tracked, result);
|
|
340
336
|
});
|
|
341
337
|
|
|
342
338
|
effect(() => {
|
|
@@ -344,11 +340,9 @@ export function bindGroup(maybe_tracked) {
|
|
|
344
340
|
|
|
345
341
|
if (is_checkbox) {
|
|
346
342
|
value = value || [];
|
|
347
|
-
|
|
348
|
-
input.checked = value.includes(input.__value);
|
|
343
|
+
input.checked = value.includes(input.value);
|
|
349
344
|
} else {
|
|
350
|
-
|
|
351
|
-
input.checked = value === input.__value;
|
|
345
|
+
input.checked = value === input.value;
|
|
352
346
|
}
|
|
353
347
|
});
|
|
354
348
|
|
|
@@ -530,6 +524,24 @@ export function bindTextContent(maybe_tracked) {
|
|
|
530
524
|
return bind_content_editable(maybe_tracked, 'textContent');
|
|
531
525
|
}
|
|
532
526
|
|
|
527
|
+
/**
|
|
528
|
+
* @param {unknown} maybe_tracked
|
|
529
|
+
* @returns {(node: HTMLInputElement) => void}
|
|
530
|
+
*/
|
|
531
|
+
export function bindFiles(maybe_tracked) {
|
|
532
|
+
if (!is_tracked_object(maybe_tracked)) {
|
|
533
|
+
throw not_tracked_type_error('bindFiles()');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const tracked = /** @type {Tracked} */ (maybe_tracked);
|
|
537
|
+
|
|
538
|
+
return (input) => {
|
|
539
|
+
return on(input, 'change', () => {
|
|
540
|
+
set(tracked, input.files);
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
533
545
|
/**
|
|
534
546
|
* Syntactic sugar for binding a HTMLElement with {ref fn}
|
|
535
547
|
* @param {unknown} maybe_tracked
|
|
@@ -163,7 +163,13 @@ export function handle_event_propagation(event) {
|
|
|
163
163
|
var delegated = current_target['__' + event_name];
|
|
164
164
|
|
|
165
165
|
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
|
|
166
|
-
delegated
|
|
166
|
+
if (is_array(delegated)) {
|
|
167
|
+
for (var i = 0; i < delegated.length; i++) {
|
|
168
|
+
delegated[i].call(current_target, event);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
delegated.call(current_target, event);
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
} catch (error) {
|
|
169
175
|
if (throw_error) {
|
|
@@ -235,10 +241,31 @@ function create_event(event_name, dom, handler, options) {
|
|
|
235
241
|
|
|
236
242
|
if (is_delegated) {
|
|
237
243
|
var prop = '__' + event_name;
|
|
238
|
-
/** @type {DelegatedEventTarget} */ (dom)
|
|
244
|
+
var target = /** @type {DelegatedEventTarget} */ (dom);
|
|
245
|
+
var current = target[prop];
|
|
246
|
+
|
|
247
|
+
if (current === undefined) {
|
|
248
|
+
target[prop] = handler;
|
|
249
|
+
} else if (is_array(current)) {
|
|
250
|
+
if (!current.includes(handler)) {
|
|
251
|
+
current.push(handler);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
if (current !== handler) {
|
|
255
|
+
target[prop] = [current, handler];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
239
259
|
delegate([event_name]);
|
|
240
260
|
return () => {
|
|
241
|
-
|
|
261
|
+
var handlers = target[prop];
|
|
262
|
+
if (is_array(handlers)) {
|
|
263
|
+
var filtered = handlers.filter((h) => h !== handler);
|
|
264
|
+
target[prop] =
|
|
265
|
+
filtered.length === 0 ? undefined : filtered.length === 1 ? filtered[0] : filtered;
|
|
266
|
+
} else {
|
|
267
|
+
target[prop] = undefined;
|
|
268
|
+
}
|
|
242
269
|
};
|
|
243
270
|
}
|
|
244
271
|
|
|
@@ -129,7 +129,7 @@ function apply_styles(element, new_styles, prev) {
|
|
|
129
129
|
* @param {string} key
|
|
130
130
|
* @param {any} value
|
|
131
131
|
* @param {Record<string, (() => void) | undefined>} remove_listeners
|
|
132
|
-
* @param {Record<string, any>} prev
|
|
132
|
+
* @param {Record<string | symbol, any>} prev
|
|
133
133
|
*/
|
|
134
134
|
function set_attribute_helper(element, key, value, remove_listeners, prev) {
|
|
135
135
|
if (key === 'class') {
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { track, flushSync, on } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('on() event handler', () => {
|
|
4
|
+
it('should attach multiple handlers via onClick attribute (delegated)', () => {
|
|
5
|
+
component Basic() {
|
|
6
|
+
let count1 = track(0);
|
|
7
|
+
let count2 = track(0);
|
|
8
|
+
|
|
9
|
+
<button
|
|
10
|
+
onClick={() => {
|
|
11
|
+
@count1++;
|
|
12
|
+
@count2++;
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
{'Click me'}
|
|
16
|
+
</button>
|
|
17
|
+
<div class="count1">{@count1}</div>
|
|
18
|
+
<div class="count2">{@count2}</div>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(Basic);
|
|
22
|
+
|
|
23
|
+
const button = container.querySelector('button');
|
|
24
|
+
const count1Div = container.querySelector('.count1');
|
|
25
|
+
const count2Div = container.querySelector('.count2');
|
|
26
|
+
|
|
27
|
+
expect(count1Div.textContent).toBe('0');
|
|
28
|
+
expect(count2Div.textContent).toBe('0');
|
|
29
|
+
|
|
30
|
+
button.click();
|
|
31
|
+
flushSync();
|
|
32
|
+
expect(count1Div.textContent).toBe('1');
|
|
33
|
+
expect(count2Div.textContent).toBe('1');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should attach and remove a single event handler', () => {
|
|
37
|
+
component Basic() {
|
|
38
|
+
let count = track(0);
|
|
39
|
+
|
|
40
|
+
const setupListener = (node) => {
|
|
41
|
+
const remove = on(node, 'click', () => {
|
|
42
|
+
@count++;
|
|
43
|
+
});
|
|
44
|
+
return remove;
|
|
45
|
+
};
|
|
46
|
+
<button {ref setupListener}>{'Click me'}</button>
|
|
47
|
+
<div class="count">{@count}</div>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
render(Basic);
|
|
51
|
+
flushSync();
|
|
52
|
+
|
|
53
|
+
const button = container.querySelector('button');
|
|
54
|
+
const countDiv = container.querySelector('.count');
|
|
55
|
+
|
|
56
|
+
expect(countDiv.textContent).toBe('0');
|
|
57
|
+
|
|
58
|
+
button.click();
|
|
59
|
+
flushSync();
|
|
60
|
+
expect(countDiv.textContent).toBe('1');
|
|
61
|
+
|
|
62
|
+
button.click();
|
|
63
|
+
flushSync();
|
|
64
|
+
expect(countDiv.textContent).toBe('2');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle multiple different event types on same element', () => {
|
|
68
|
+
component Basic() {
|
|
69
|
+
let clickCount = track(0);
|
|
70
|
+
let mousedownCount = track(0);
|
|
71
|
+
|
|
72
|
+
const setupListeners = (node) => {
|
|
73
|
+
const remove1 = on(node, 'click', () => {
|
|
74
|
+
@clickCount++;
|
|
75
|
+
});
|
|
76
|
+
const remove2 = on(node, 'mousedown', () => {
|
|
77
|
+
@mousedownCount++;
|
|
78
|
+
});
|
|
79
|
+
return () => {
|
|
80
|
+
remove1();
|
|
81
|
+
remove2();
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
<button {ref setupListeners}>{'Test'}</button>
|
|
85
|
+
<div class="click-count">{@clickCount}</div>
|
|
86
|
+
<div class="mousedown-count">{@mousedownCount}</div>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
render(Basic);
|
|
90
|
+
flushSync();
|
|
91
|
+
|
|
92
|
+
const button = container.querySelector('button');
|
|
93
|
+
const clickDiv = container.querySelector('.click-count');
|
|
94
|
+
const mousedownDiv = container.querySelector('.mousedown-count');
|
|
95
|
+
|
|
96
|
+
expect(clickDiv.textContent).toBe('0');
|
|
97
|
+
expect(mousedownDiv.textContent).toBe('0');
|
|
98
|
+
|
|
99
|
+
button.click();
|
|
100
|
+
flushSync();
|
|
101
|
+
expect(clickDiv.textContent).toBe('1');
|
|
102
|
+
expect(mousedownDiv.textContent).toBe('0'); // click() doesn't trigger mousedown
|
|
103
|
+
|
|
104
|
+
button.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
105
|
+
flushSync();
|
|
106
|
+
expect(clickDiv.textContent).toBe('1');
|
|
107
|
+
expect(mousedownDiv.textContent).toBe('1');
|
|
108
|
+
|
|
109
|
+
// Click again to verify both handlers still work
|
|
110
|
+
button.click();
|
|
111
|
+
flushSync();
|
|
112
|
+
expect(clickDiv.textContent).toBe('2');
|
|
113
|
+
expect(mousedownDiv.textContent).toBe('1'); // Still only incremented by mousedown events
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle multiple handlers for same event type on same element', () => {
|
|
117
|
+
component Basic() {
|
|
118
|
+
let callOrder = track<number[]>([]);
|
|
119
|
+
|
|
120
|
+
const setupListeners = (node) => {
|
|
121
|
+
const remove1 = on(node, 'click', () => {
|
|
122
|
+
@callOrder = [...@callOrder, 1];
|
|
123
|
+
});
|
|
124
|
+
const remove2 = on(node, 'click', () => {
|
|
125
|
+
@callOrder = [...@callOrder, 2];
|
|
126
|
+
});
|
|
127
|
+
const remove3 = on(node, 'click', () => {
|
|
128
|
+
@callOrder = [...@callOrder, 3];
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
remove1();
|
|
132
|
+
remove2();
|
|
133
|
+
remove3();
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
<button {ref setupListeners}>{'Click me'}</button>
|
|
137
|
+
<div class="order">{@callOrder.join(',')}</div>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
render(Basic);
|
|
141
|
+
flushSync();
|
|
142
|
+
|
|
143
|
+
const button = container.querySelector('button');
|
|
144
|
+
const orderDiv = container.querySelector('.order');
|
|
145
|
+
|
|
146
|
+
expect(orderDiv.textContent).toBe('');
|
|
147
|
+
|
|
148
|
+
button.click();
|
|
149
|
+
flushSync();
|
|
150
|
+
expect(orderDiv.textContent).toBe('1,2,3');
|
|
151
|
+
|
|
152
|
+
// Click again to verify order is consistent
|
|
153
|
+
button.click();
|
|
154
|
+
flushSync();
|
|
155
|
+
expect(orderDiv.textContent).toBe('1,2,3,1,2,3');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should remove specific handler without affecting others', () => {
|
|
159
|
+
component Basic() {
|
|
160
|
+
let handler1Called = track(0);
|
|
161
|
+
let handler2Called = track(0);
|
|
162
|
+
let handler3Called = track(0);
|
|
163
|
+
let removeHandler2;
|
|
164
|
+
|
|
165
|
+
const setupListeners = (node) => {
|
|
166
|
+
const remove1 = on(node, 'click', () => {
|
|
167
|
+
@handler1Called++;
|
|
168
|
+
});
|
|
169
|
+
removeHandler2 = on(node, 'click', () => {
|
|
170
|
+
@handler2Called++;
|
|
171
|
+
});
|
|
172
|
+
const remove3 = on(node, 'click', () => {
|
|
173
|
+
@handler3Called++;
|
|
174
|
+
});
|
|
175
|
+
return () => {
|
|
176
|
+
remove1();
|
|
177
|
+
removeHandler2?.();
|
|
178
|
+
remove3();
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
<div>
|
|
182
|
+
<button class="test-btn" {ref setupListeners}>{'Click me'}</button>
|
|
183
|
+
<button
|
|
184
|
+
class="remove-btn"
|
|
185
|
+
onClick={() => {
|
|
186
|
+
removeHandler2?.();
|
|
187
|
+
removeHandler2 = undefined;
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{'Remove handler 2'}
|
|
191
|
+
</button>
|
|
192
|
+
<div class="h1">{@handler1Called}</div>
|
|
193
|
+
<div class="h2">{@handler2Called}</div>
|
|
194
|
+
<div class="h3">{@handler3Called}</div>
|
|
195
|
+
</div>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
render(Basic);
|
|
199
|
+
flushSync();
|
|
200
|
+
|
|
201
|
+
const testBtn = container.querySelector('.test-btn');
|
|
202
|
+
const removeBtn = container.querySelector('.remove-btn');
|
|
203
|
+
const h1Div = container.querySelector('.h1');
|
|
204
|
+
const h2Div = container.querySelector('.h2');
|
|
205
|
+
const h3Div = container.querySelector('.h3');
|
|
206
|
+
|
|
207
|
+
// All handlers should be called initially
|
|
208
|
+
testBtn.click();
|
|
209
|
+
flushSync();
|
|
210
|
+
expect(h1Div.textContent).toBe('1');
|
|
211
|
+
expect(h2Div.textContent).toBe('1');
|
|
212
|
+
expect(h3Div.textContent).toBe('1');
|
|
213
|
+
|
|
214
|
+
// Remove handler 2
|
|
215
|
+
removeBtn.click();
|
|
216
|
+
flushSync();
|
|
217
|
+
|
|
218
|
+
// Only handlers 1 and 3 should be called
|
|
219
|
+
testBtn.click();
|
|
220
|
+
flushSync();
|
|
221
|
+
expect(h1Div.textContent).toBe('2');
|
|
222
|
+
expect(h2Div.textContent).toBe('1'); // Should not increment
|
|
223
|
+
expect(h3Div.textContent).toBe('2');
|
|
224
|
+
|
|
225
|
+
// Verify again
|
|
226
|
+
testBtn.click();
|
|
227
|
+
flushSync();
|
|
228
|
+
expect(h1Div.textContent).toBe('3');
|
|
229
|
+
expect(h2Div.textContent).toBe('1'); // Still should not increment
|
|
230
|
+
expect(h3Div.textContent).toBe('3');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it(
|
|
234
|
+
'should handle change event with multiple handlers (like bindChecked and bindIndeterminate)',
|
|
235
|
+
() => {
|
|
236
|
+
component Basic() {
|
|
237
|
+
let checked = track(false);
|
|
238
|
+
let indeterminate = track(true);
|
|
239
|
+
|
|
240
|
+
const setupListeners = (node) => {
|
|
241
|
+
node.indeterminate = @indeterminate;
|
|
242
|
+
node.checked = @checked;
|
|
243
|
+
|
|
244
|
+
const remove1 = on(node, 'change', () => {
|
|
245
|
+
@checked = node.checked;
|
|
246
|
+
});
|
|
247
|
+
const remove2 = on(node, 'change', () => {
|
|
248
|
+
@indeterminate = node.indeterminate;
|
|
249
|
+
});
|
|
250
|
+
return () => {
|
|
251
|
+
remove1();
|
|
252
|
+
remove2();
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
<div>
|
|
256
|
+
<input type="checkbox" {ref setupListeners} />
|
|
257
|
+
<div class="checked">{@checked ? 'true' : 'false'}</div>
|
|
258
|
+
<div class="indeterminate">{@indeterminate ? 'true' : 'false'}</div>
|
|
259
|
+
</div>
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
render(Basic);
|
|
263
|
+
flushSync();
|
|
264
|
+
|
|
265
|
+
const input = container.querySelector('input');
|
|
266
|
+
const checkedDiv = container.querySelector('.checked');
|
|
267
|
+
const indeterminateDiv = container.querySelector('.indeterminate');
|
|
268
|
+
|
|
269
|
+
expect(checkedDiv.textContent).toBe('false');
|
|
270
|
+
expect(indeterminateDiv.textContent).toBe('true');
|
|
271
|
+
expect(input.indeterminate).toBe(true);
|
|
272
|
+
|
|
273
|
+
// Click the checkbox
|
|
274
|
+
input.click();
|
|
275
|
+
flushSync();
|
|
276
|
+
|
|
277
|
+
// Both tracked values should update
|
|
278
|
+
expect(checkedDiv.textContent).toBe('true');
|
|
279
|
+
expect(indeterminateDiv.textContent).toBe('false');
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
it('should support non-delegated events', () => {
|
|
284
|
+
component Basic() {
|
|
285
|
+
let focusCount = track(0);
|
|
286
|
+
|
|
287
|
+
const setupListener = (node) => {
|
|
288
|
+
const remove = on(node, 'focus', () => {
|
|
289
|
+
@focusCount++;
|
|
290
|
+
});
|
|
291
|
+
return remove;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
<input {ref setupListener} />
|
|
295
|
+
<div class="focus-count">{@focusCount}</div>
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
render(Basic);
|
|
299
|
+
flushSync();
|
|
300
|
+
|
|
301
|
+
const input = container.querySelector('input');
|
|
302
|
+
const focusDiv = container.querySelector('.focus-count');
|
|
303
|
+
|
|
304
|
+
expect(focusDiv.textContent).toBe('0');
|
|
305
|
+
|
|
306
|
+
input.dispatchEvent(new Event('focus'));
|
|
307
|
+
flushSync();
|
|
308
|
+
expect(focusDiv.textContent).toBe('1');
|
|
309
|
+
|
|
310
|
+
input.dispatchEvent(new Event('focus'));
|
|
311
|
+
flushSync();
|
|
312
|
+
expect(focusDiv.textContent).toBe('2');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should handle removal of all handlers for same event type', () => {
|
|
316
|
+
component Basic() {
|
|
317
|
+
let count = track(0);
|
|
318
|
+
let remove1, remove2, remove3;
|
|
319
|
+
|
|
320
|
+
const setupListeners = (node) => {
|
|
321
|
+
remove1 = on(node, 'click', () => {
|
|
322
|
+
@count++;
|
|
323
|
+
});
|
|
324
|
+
remove2 = on(node, 'click', () => {
|
|
325
|
+
@count += 10;
|
|
326
|
+
});
|
|
327
|
+
remove3 = on(node, 'click', () => {
|
|
328
|
+
@count += 100;
|
|
329
|
+
});
|
|
330
|
+
return () => {
|
|
331
|
+
remove1?.();
|
|
332
|
+
remove2?.();
|
|
333
|
+
remove3?.();
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
<div>
|
|
337
|
+
<button class="test-btn" {ref setupListeners}>{'Click me'}</button>
|
|
338
|
+
<button
|
|
339
|
+
class="remove-all"
|
|
340
|
+
onClick={() => {
|
|
341
|
+
remove1?.();
|
|
342
|
+
remove2?.();
|
|
343
|
+
remove3?.();
|
|
344
|
+
remove1 = remove2 = remove3 = undefined;
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
{'Remove all'}
|
|
348
|
+
</button>
|
|
349
|
+
<div class="count">{@count}</div>
|
|
350
|
+
</div>
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
render(Basic);
|
|
354
|
+
flushSync();
|
|
355
|
+
|
|
356
|
+
const testBtn = container.querySelector('.test-btn');
|
|
357
|
+
const removeAllBtn = container.querySelector('.remove-all');
|
|
358
|
+
const countDiv = container.querySelector('.count');
|
|
359
|
+
|
|
360
|
+
expect(countDiv.textContent).toBe('0');
|
|
361
|
+
|
|
362
|
+
// All three handlers should fire (1 + 10 + 100 = 111)
|
|
363
|
+
testBtn.click();
|
|
364
|
+
flushSync();
|
|
365
|
+
expect(countDiv.textContent).toBe('111');
|
|
366
|
+
|
|
367
|
+
// Remove all handlers
|
|
368
|
+
removeAllBtn.click();
|
|
369
|
+
flushSync();
|
|
370
|
+
|
|
371
|
+
// No handlers should fire
|
|
372
|
+
testBtn.click();
|
|
373
|
+
flushSync();
|
|
374
|
+
expect(countDiv.textContent).toBe('111'); // Should remain unchanged
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should not add duplicate handlers when same handler is attached multiple times', () => {
|
|
378
|
+
component Basic() {
|
|
379
|
+
let count = track(0);
|
|
380
|
+
|
|
381
|
+
const sharedHandler = () => {
|
|
382
|
+
@count++;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const setupListeners = (node) => {
|
|
386
|
+
// Attach the same handler multiple times
|
|
387
|
+
const remove1 = on(node, 'click', sharedHandler);
|
|
388
|
+
const remove2 = on(node, 'click', sharedHandler);
|
|
389
|
+
const remove3 = on(node, 'click', sharedHandler);
|
|
390
|
+
|
|
391
|
+
return () => {
|
|
392
|
+
remove1?.();
|
|
393
|
+
remove2?.();
|
|
394
|
+
remove3?.();
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
<button {ref setupListeners}>{'Click me'}</button>
|
|
399
|
+
<div class="count">{@count}</div>
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
render(Basic);
|
|
403
|
+
flushSync();
|
|
404
|
+
|
|
405
|
+
const button = container.querySelector('button');
|
|
406
|
+
const countDiv = container.querySelector('.count');
|
|
407
|
+
|
|
408
|
+
expect(countDiv.textContent).toBe('0');
|
|
409
|
+
|
|
410
|
+
// Handler should only be called once per click, not three times
|
|
411
|
+
button.click();
|
|
412
|
+
flushSync();
|
|
413
|
+
expect(countDiv.textContent).toBe('1');
|
|
414
|
+
|
|
415
|
+
button.click();
|
|
416
|
+
flushSync();
|
|
417
|
+
expect(countDiv.textContent).toBe('2');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should allow duplicate handlers when delegated is false (no deduplication)', () => {
|
|
421
|
+
component Basic() {
|
|
422
|
+
let count = track(0);
|
|
423
|
+
|
|
424
|
+
const sharedHandler = () => {
|
|
425
|
+
@count++;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const setupListeners = (node) => {
|
|
429
|
+
// Attach the same handler multiple times with delegated: false
|
|
430
|
+
const remove1 = on(node, 'click', sharedHandler, { delegated: false });
|
|
431
|
+
const remove2 = on(node, 'click', sharedHandler, { delegated: false });
|
|
432
|
+
const remove3 = on(node, 'click', sharedHandler, { delegated: false });
|
|
433
|
+
|
|
434
|
+
return () => {
|
|
435
|
+
remove1?.();
|
|
436
|
+
remove2?.();
|
|
437
|
+
remove3?.();
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
<button {ref setupListeners}>{'Click me'}</button>
|
|
442
|
+
<div class="count">{@count}</div>
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
render(Basic);
|
|
446
|
+
flushSync();
|
|
447
|
+
|
|
448
|
+
const button = container.querySelector('button');
|
|
449
|
+
const countDiv = container.querySelector('.count');
|
|
450
|
+
|
|
451
|
+
expect(countDiv.textContent).toBe('0');
|
|
452
|
+
|
|
453
|
+
// Non-delegated events use addEventListener directly, which DOES allow duplicates
|
|
454
|
+
// So handler should be called 3 times per click
|
|
455
|
+
button.click();
|
|
456
|
+
flushSync();
|
|
457
|
+
expect(countDiv.textContent).toBe('3');
|
|
458
|
+
|
|
459
|
+
button.click();
|
|
460
|
+
flushSync();
|
|
461
|
+
expect(countDiv.textContent).toBe('6');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should fire capture event on parent before bubbling event on child', () => {
|
|
465
|
+
component Basic() {
|
|
466
|
+
let callOrder = track<string[]>([]);
|
|
467
|
+
|
|
468
|
+
const parentCaptureHandler = () => {
|
|
469
|
+
@callOrder = [...@callOrder, 'parent-capture'];
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const childBubbleHandler = () => {
|
|
473
|
+
@callOrder = [...@callOrder, 'child-bubble'];
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const setupParent = (node) => {
|
|
477
|
+
return on(node, 'clickCapture', parentCaptureHandler);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const setupChild = (node) => {
|
|
481
|
+
return on(node, 'click', childBubbleHandler);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
<div {ref setupParent} class="parent">
|
|
485
|
+
<button {ref setupChild} class="child">{'Click me'}</button>
|
|
486
|
+
</div>
|
|
487
|
+
<div class="order">{@callOrder.join(',')}</div>
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
render(Basic);
|
|
491
|
+
flushSync();
|
|
492
|
+
|
|
493
|
+
const button = container.querySelector('.child');
|
|
494
|
+
const orderDiv = container.querySelector('.order');
|
|
495
|
+
|
|
496
|
+
expect(orderDiv.textContent).toBe('');
|
|
497
|
+
|
|
498
|
+
// Capture phase happens first (parent), then bubbling phase (child)
|
|
499
|
+
button.click();
|
|
500
|
+
flushSync();
|
|
501
|
+
expect(orderDiv.textContent).toBe('parent-capture,child-bubble');
|
|
502
|
+
|
|
503
|
+
// Click again to verify order is consistent
|
|
504
|
+
button.click();
|
|
505
|
+
flushSync();
|
|
506
|
+
expect(orderDiv.textContent).toBe('parent-capture,child-bubble,parent-capture,child-bubble');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('should fire handler only once when once option is true', () => {
|
|
510
|
+
component Basic() {
|
|
511
|
+
let count = track(0);
|
|
512
|
+
let permanentCount = track(0);
|
|
513
|
+
|
|
514
|
+
const setupListeners = (node) => {
|
|
515
|
+
const onceHandler = on(node, 'click', () => {
|
|
516
|
+
@count++;
|
|
517
|
+
}, { once: true });
|
|
518
|
+
|
|
519
|
+
const permanentHandler = on(node, 'click', () => {
|
|
520
|
+
@permanentCount++;
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return () => {
|
|
524
|
+
onceHandler?.();
|
|
525
|
+
permanentHandler?.();
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
<button {ref setupListeners}>{'Click me'}</button>
|
|
530
|
+
<div class="once-count">{@count}</div>
|
|
531
|
+
<div class="permanent-count">{@permanentCount}</div>
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
render(Basic);
|
|
535
|
+
flushSync();
|
|
536
|
+
|
|
537
|
+
const button = container.querySelector('button');
|
|
538
|
+
const onceDiv = container.querySelector('.once-count');
|
|
539
|
+
const permanentDiv = container.querySelector('.permanent-count');
|
|
540
|
+
|
|
541
|
+
expect(onceDiv.textContent).toBe('0');
|
|
542
|
+
expect(permanentDiv.textContent).toBe('0');
|
|
543
|
+
|
|
544
|
+
// First click: both handlers should fire
|
|
545
|
+
button.click();
|
|
546
|
+
flushSync();
|
|
547
|
+
expect(onceDiv.textContent).toBe('1');
|
|
548
|
+
expect(permanentDiv.textContent).toBe('1');
|
|
549
|
+
|
|
550
|
+
// Second click: only permanent handler should fire
|
|
551
|
+
button.click();
|
|
552
|
+
flushSync();
|
|
553
|
+
expect(onceDiv.textContent).toBe('1'); // Still 1
|
|
554
|
+
expect(permanentDiv.textContent).toBe('2');
|
|
555
|
+
|
|
556
|
+
// Third click: only permanent handler should fire
|
|
557
|
+
button.click();
|
|
558
|
+
flushSync();
|
|
559
|
+
expect(onceDiv.textContent).toBe('1'); // Still 1
|
|
560
|
+
expect(permanentDiv.textContent).toBe('3');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
bindInnerText,
|
|
19
19
|
bindTextContent,
|
|
20
20
|
bindNode,
|
|
21
|
+
bindFiles,
|
|
21
22
|
} from 'ripple';
|
|
22
23
|
|
|
23
24
|
// Mock ResizeObserver for testing
|
|
@@ -57,9 +58,37 @@ function triggerResize(element: Element, entry: Partial<ResizeObserverEntry>) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Mock DataTransfer for testing file inputs
|
|
62
|
+
class MockDataTransfer {
|
|
63
|
+
items: MockDataTransferItemList;
|
|
64
|
+
files: FileList;
|
|
65
|
+
|
|
66
|
+
constructor() {
|
|
67
|
+
this.items = new MockDataTransferItemList();
|
|
68
|
+
this.files = this.items.files;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class MockDataTransferItemList {
|
|
73
|
+
_files: File[] = [];
|
|
74
|
+
|
|
75
|
+
get files(): FileList {
|
|
76
|
+
return this._files as any as FileList;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
add(file: File): void {
|
|
80
|
+
this._files.push(file);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get length(): number {
|
|
84
|
+
return this._files.length;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
// Setup ResizeObserver mock
|
|
61
89
|
beforeAll(() => {
|
|
62
90
|
(global as any).ResizeObserver = createMockResizeObserver;
|
|
91
|
+
(global as any).DataTransfer = MockDataTransfer;
|
|
63
92
|
});
|
|
64
93
|
|
|
65
94
|
afterAll(() => {
|
|
@@ -1227,6 +1256,265 @@ describe('bindNode', () => {
|
|
|
1227
1256
|
});
|
|
1228
1257
|
});
|
|
1229
1258
|
|
|
1259
|
+
describe('bindFiles', () => {
|
|
1260
|
+
it('should bind files from file input', () => {
|
|
1261
|
+
const logs: FileList[] = [];
|
|
1262
|
+
|
|
1263
|
+
component App() {
|
|
1264
|
+
const files = track(null);
|
|
1265
|
+
|
|
1266
|
+
effect(() => {
|
|
1267
|
+
@files;
|
|
1268
|
+
if (@files) logs.push(@files);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
<input type="file" multiple {ref bindFiles(files)} />
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
render(App);
|
|
1275
|
+
flushSync();
|
|
1276
|
+
|
|
1277
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1278
|
+
|
|
1279
|
+
// Create mock FileList using DataTransfer
|
|
1280
|
+
const dt = new DataTransfer();
|
|
1281
|
+
const file1 = new File(['content1'], 'file1.txt', { type: 'text/plain' });
|
|
1282
|
+
const file2 = new File(['content2'], 'file2.txt', { type: 'text/plain' });
|
|
1283
|
+
dt.items.add(file1);
|
|
1284
|
+
dt.items.add(file2);
|
|
1285
|
+
|
|
1286
|
+
// Simulate file selection
|
|
1287
|
+
Object.defineProperty(input, 'files', {
|
|
1288
|
+
value: dt.files,
|
|
1289
|
+
writable: true,
|
|
1290
|
+
});
|
|
1291
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1292
|
+
flushSync();
|
|
1293
|
+
|
|
1294
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
1295
|
+
const lastFiles = logs[logs.length - 1];
|
|
1296
|
+
expect(lastFiles.length).toBe(2);
|
|
1297
|
+
expect(lastFiles[0].name).toBe('file1.txt');
|
|
1298
|
+
expect(lastFiles[1].name).toBe('file2.txt');
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it('should update tracked value when files are selected', () => {
|
|
1302
|
+
let capturedFiles: FileList | null = null;
|
|
1303
|
+
|
|
1304
|
+
component App() {
|
|
1305
|
+
const files = track(null);
|
|
1306
|
+
|
|
1307
|
+
effect(() => {
|
|
1308
|
+
capturedFiles = @files;
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
render(App);
|
|
1315
|
+
flushSync();
|
|
1316
|
+
|
|
1317
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1318
|
+
|
|
1319
|
+
// Create mock file
|
|
1320
|
+
const dt = new DataTransfer();
|
|
1321
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
1322
|
+
dt.items.add(file);
|
|
1323
|
+
|
|
1324
|
+
Object.defineProperty(input, 'files', {
|
|
1325
|
+
value: dt.files,
|
|
1326
|
+
writable: true,
|
|
1327
|
+
});
|
|
1328
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1329
|
+
flushSync();
|
|
1330
|
+
|
|
1331
|
+
expect(capturedFiles).not.toBeNull();
|
|
1332
|
+
expect(capturedFiles?.length).toBe(1);
|
|
1333
|
+
expect(capturedFiles?.[0].name).toBe('test.txt');
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('should allow clearing files via input.files', () => {
|
|
1337
|
+
let capturedFiles: FileList | null = null;
|
|
1338
|
+
|
|
1339
|
+
component App() {
|
|
1340
|
+
const files = track(null);
|
|
1341
|
+
const input = track(null);
|
|
1342
|
+
|
|
1343
|
+
effect(() => {
|
|
1344
|
+
capturedFiles = @files;
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
<div>
|
|
1348
|
+
<input type="file" {ref bindFiles(files)} {ref bindNode(input)} />
|
|
1349
|
+
<button
|
|
1350
|
+
onClick={() => {
|
|
1351
|
+
if (@input) {
|
|
1352
|
+
@input.files = new DataTransfer().files;
|
|
1353
|
+
@input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1354
|
+
}
|
|
1355
|
+
}}
|
|
1356
|
+
>
|
|
1357
|
+
{'Clear'}
|
|
1358
|
+
</button>
|
|
1359
|
+
</div>
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
render(App);
|
|
1363
|
+
flushSync();
|
|
1364
|
+
|
|
1365
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1366
|
+
const button = container.querySelector('button') as HTMLButtonElement;
|
|
1367
|
+
|
|
1368
|
+
// Add a file first
|
|
1369
|
+
const dt = new DataTransfer();
|
|
1370
|
+
const file = new File(['content'], 'file.txt', { type: 'text/plain' });
|
|
1371
|
+
dt.items.add(file);
|
|
1372
|
+
|
|
1373
|
+
Object.defineProperty(input, 'files', {
|
|
1374
|
+
value: dt.files,
|
|
1375
|
+
writable: true,
|
|
1376
|
+
});
|
|
1377
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1378
|
+
flushSync();
|
|
1379
|
+
|
|
1380
|
+
expect(capturedFiles?.length).toBe(1);
|
|
1381
|
+
|
|
1382
|
+
// Clear via button
|
|
1383
|
+
button.click();
|
|
1384
|
+
flushSync();
|
|
1385
|
+
|
|
1386
|
+
expect(capturedFiles?.length).toBe(0);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('should handle multiple file selections', () => {
|
|
1390
|
+
const fileCounts: number[] = [];
|
|
1391
|
+
|
|
1392
|
+
component App() {
|
|
1393
|
+
const files = track(null);
|
|
1394
|
+
|
|
1395
|
+
effect(() => {
|
|
1396
|
+
@files;
|
|
1397
|
+
if (@files) {
|
|
1398
|
+
fileCounts.push(@files.length);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
<input type="file" multiple {ref bindFiles(files)} />
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
render(App);
|
|
1406
|
+
flushSync();
|
|
1407
|
+
|
|
1408
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1409
|
+
|
|
1410
|
+
// First selection: 2 files
|
|
1411
|
+
const dt1 = new DataTransfer();
|
|
1412
|
+
dt1.items.add(new File(['a'], 'a.txt'));
|
|
1413
|
+
dt1.items.add(new File(['b'], 'b.txt'));
|
|
1414
|
+
|
|
1415
|
+
Object.defineProperty(input, 'files', {
|
|
1416
|
+
value: dt1.files,
|
|
1417
|
+
writable: true,
|
|
1418
|
+
});
|
|
1419
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1420
|
+
flushSync();
|
|
1421
|
+
|
|
1422
|
+
// Second selection: 3 files
|
|
1423
|
+
const dt2 = new DataTransfer();
|
|
1424
|
+
dt2.items.add(new File(['x'], 'x.txt'));
|
|
1425
|
+
dt2.items.add(new File(['y'], 'y.txt'));
|
|
1426
|
+
dt2.items.add(new File(['z'], 'z.txt'));
|
|
1427
|
+
|
|
1428
|
+
Object.defineProperty(input, 'files', {
|
|
1429
|
+
value: dt2.files,
|
|
1430
|
+
writable: true,
|
|
1431
|
+
});
|
|
1432
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1433
|
+
flushSync();
|
|
1434
|
+
|
|
1435
|
+
expect(fileCounts).toEqual([2, 3]);
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
it('should handle file input without multiple attribute', () => {
|
|
1439
|
+
let capturedFile: File | null = null;
|
|
1440
|
+
|
|
1441
|
+
component App() {
|
|
1442
|
+
const files = track(null);
|
|
1443
|
+
|
|
1444
|
+
effect(() => {
|
|
1445
|
+
@files;
|
|
1446
|
+
if (@files && @files.length > 0) {
|
|
1447
|
+
capturedFile = @files[0];
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
render(App);
|
|
1455
|
+
flushSync();
|
|
1456
|
+
|
|
1457
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1458
|
+
|
|
1459
|
+
const dt = new DataTransfer();
|
|
1460
|
+
const file = new File(['single file content'], 'single.pdf', { type: 'application/pdf' });
|
|
1461
|
+
dt.items.add(file);
|
|
1462
|
+
|
|
1463
|
+
Object.defineProperty(input, 'files', {
|
|
1464
|
+
value: dt.files,
|
|
1465
|
+
writable: true,
|
|
1466
|
+
});
|
|
1467
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1468
|
+
flushSync();
|
|
1469
|
+
|
|
1470
|
+
expect(capturedFile).not.toBeNull();
|
|
1471
|
+
expect(capturedFile?.name).toBe('single.pdf');
|
|
1472
|
+
expect(capturedFile?.type).toBe('application/pdf');
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('should handle empty file selection', () => {
|
|
1476
|
+
const logs: (FileList | null)[] = [];
|
|
1477
|
+
|
|
1478
|
+
component App() {
|
|
1479
|
+
const files = track(null);
|
|
1480
|
+
|
|
1481
|
+
effect(() => {
|
|
1482
|
+
logs.push(@files);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
<input type="file" {ref bindFiles(files)} />
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
render(App);
|
|
1489
|
+
flushSync();
|
|
1490
|
+
|
|
1491
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
1492
|
+
|
|
1493
|
+
// Select a file
|
|
1494
|
+
const dt = new DataTransfer();
|
|
1495
|
+
dt.items.add(new File(['test'], 'test.txt'));
|
|
1496
|
+
|
|
1497
|
+
Object.defineProperty(input, 'files', {
|
|
1498
|
+
value: dt.files,
|
|
1499
|
+
writable: true,
|
|
1500
|
+
});
|
|
1501
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1502
|
+
flushSync();
|
|
1503
|
+
|
|
1504
|
+
// Clear selection
|
|
1505
|
+
Object.defineProperty(input, 'files', {
|
|
1506
|
+
value: new DataTransfer().files,
|
|
1507
|
+
writable: true,
|
|
1508
|
+
});
|
|
1509
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1510
|
+
flushSync();
|
|
1511
|
+
|
|
1512
|
+
expect(logs.length).toBeGreaterThan(1);
|
|
1513
|
+
const lastFiles = logs[logs.length - 1];
|
|
1514
|
+
expect(lastFiles?.length).toBe(0);
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1230
1518
|
describe('error handling', () => {
|
|
1231
1519
|
it('should throw error when bindValue receives non-tracked object', () => {
|
|
1232
1520
|
expect(() => {
|
|
@@ -1371,4 +1659,13 @@ describe('error handling', () => {
|
|
|
1371
1659
|
render(App);
|
|
1372
1660
|
}).toThrow('bindNode() argument is not a tracked object');
|
|
1373
1661
|
});
|
|
1662
|
+
|
|
1663
|
+
it('should throw error when bindFiles receives non-tracked object', () => {
|
|
1664
|
+
expect(() => {
|
|
1665
|
+
component App() {
|
|
1666
|
+
<input type="file" {ref bindFiles({ value: null })} />
|
|
1667
|
+
}
|
|
1668
|
+
render(App);
|
|
1669
|
+
}).toThrow('bindFiles() argument is not a tracked object');
|
|
1670
|
+
});
|
|
1374
1671
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -307,3 +307,5 @@ export declare function bindOffsetHeight<V>(tracked: Tracked<V>): (node: HTMLEle
|
|
|
307
307
|
export declare function bindOffsetWidth<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
308
308
|
|
|
309
309
|
export declare function bindIndeterminate<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
310
|
+
|
|
311
|
+
export declare function bindFiles<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|