Skip to content

Commit 868a5fe

Browse files
committed
- support multi-select
- verify that not allow listed fields are actually filtered
1 parent 0245377 commit 868a5fe

File tree

2 files changed

+135
-1
lines changed

2 files changed

+135
-1
lines changed

packages/html/src/render.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,6 @@ const allowListedEventTargetProperties = [
338338
"checked", // checkbox
339339
"selected", // option
340340
"selectedIndex", // select
341-
"selectedOptions", // select, multiple
342341
];
343342

344343
/**
@@ -368,9 +367,19 @@ export function serializableEvent<T>(event: Event): T {
368367
for (const property of allowListedEventTargetProperties) {
369368
targetObject[property] = event.target?.[property as keyof EventTarget];
370369
}
370+
if (
371+
event.target instanceof HTMLSelectElement && event.target.selectedOptions
372+
) {
373+
// To support multiple selections, we create serializable option elements
374+
targetObject.selectedOptions = Array.from(event.target.selectedOptions).map(
375+
(option) => ({ value: option.value }),
376+
);
377+
}
371378
if (Object.keys(targetObject).length > 0) eventObject.target = targetObject;
372379

373380
if ((event as CustomEvent).detail !== undefined) {
381+
// Could be anything, but should only come from our own custom elements.
382+
// Step below will remove any direct references.
374383
eventObject.detail = (event as CustomEvent).detail;
375384
}
376385

packages/html/test/render.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ beforeEach(() => {
1818
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
1919
globalThis.MouseEvent = dom.window.MouseEvent;
2020
globalThis.CustomEvent = dom.window.CustomEvent;
21+
globalThis.HTMLSelectElement = dom.window.HTMLSelectElement;
2122
});
2223

2324
describe("render", () => {
@@ -138,6 +139,12 @@ describe("serializableEvent", () => {
138139
true,
139140
"Result should be a plain serializable object",
140141
);
142+
// Should not include non-allow-listed fields
143+
assert.equal(
144+
"timeStamp" in (result as any),
145+
false,
146+
"Should not include timeStamp",
147+
);
141148
});
142149

143150
it("serializes a KeyboardEvent", () => {
@@ -166,6 +173,11 @@ describe("serializableEvent", () => {
166173
true,
167174
"Result should be a plain serializable object",
168175
);
176+
assert.equal(
177+
"timeStamp" in (result as any),
178+
false,
179+
"Should not include timeStamp",
180+
);
169181
});
170182

171183
it("serializes a MouseEvent", () => {
@@ -192,11 +204,17 @@ describe("serializableEvent", () => {
192204
true,
193205
"Result should be a plain serializable object",
194206
);
207+
assert.equal(
208+
"timeStamp" in (result as any),
209+
false,
210+
"Should not include timeStamp",
211+
);
195212
});
196213

197214
it("serializes an InputEvent with target value", () => {
198215
const input = document.createElement("input");
199216
input.value = "hello";
217+
input.id = "should-not-appear";
200218
const event = new InputEvent("input", {
201219
data: "h",
202220
inputType: "insertText",
@@ -214,6 +232,16 @@ describe("serializableEvent", () => {
214232
true,
215233
"Result should be a plain serializable object",
216234
);
235+
assert.equal(
236+
"timeStamp" in (result as any),
237+
false,
238+
"Should not include timeStamp",
239+
);
240+
assert.equal(
241+
(result as any).target && "id" in (result as any).target,
242+
false,
243+
"Should not include id on target",
244+
);
217245
});
218246

219247
it("serializes a CustomEvent with detail", () => {
@@ -228,5 +256,102 @@ describe("serializableEvent", () => {
228256
true,
229257
"Result should be a plain serializable object",
230258
);
259+
assert.equal(
260+
"timeStamp" in (result as any),
261+
false,
262+
"Should not include timeStamp",
263+
);
264+
});
265+
266+
it("serializes an event with HTMLSelectElement target and selectedOptions", () => {
267+
const select = document.createElement("select");
268+
select.multiple = true;
269+
select.id = "should-not-appear";
270+
// Create option elements
271+
const option1 = document.createElement("option");
272+
option1.value = "option1";
273+
option1.text = "Option 1";
274+
const option2 = document.createElement("option");
275+
option2.value = "option2";
276+
option2.text = "Option 2";
277+
const option3 = document.createElement("option");
278+
option3.value = "option3";
279+
option3.text = "Option 3";
280+
select.appendChild(option1);
281+
select.appendChild(option2);
282+
select.appendChild(option3);
283+
// Select multiple options
284+
option1.selected = true;
285+
option3.selected = true;
286+
const event = new Event("change");
287+
Object.defineProperty(event, "target", { value: select });
288+
const result = serializableEvent(event);
289+
assert.matchObject(result, {
290+
type: "change",
291+
target: {
292+
selectedOptions: [
293+
{ value: "option1" },
294+
{ value: "option3" },
295+
],
296+
},
297+
});
298+
assert.equal(
299+
isPlainSerializableObject(result),
300+
true,
301+
"Result should be a plain serializable object",
302+
);
303+
assert.equal(
304+
"timeStamp" in (result as any),
305+
false,
306+
"Should not include timeStamp",
307+
);
308+
assert.equal(
309+
(result as any).target && "id" in (result as any).target,
310+
false,
311+
"Should not include id on target",
312+
);
313+
});
314+
315+
it("serializes an event with single-select HTMLSelectElement target", () => {
316+
const select = document.createElement("select");
317+
select.multiple = false; // single select
318+
select.id = "should-not-appear";
319+
// Create option elements
320+
const option1 = document.createElement("option");
321+
option1.value = "option1";
322+
option1.text = "Option 1";
323+
const option2 = document.createElement("option");
324+
option2.value = "option2";
325+
option2.text = "Option 2";
326+
select.appendChild(option1);
327+
select.appendChild(option2);
328+
// Select single option
329+
option2.selected = true;
330+
const event = new Event("change");
331+
Object.defineProperty(event, "target", { value: select });
332+
const result = serializableEvent(event);
333+
assert.matchObject(result, {
334+
type: "change",
335+
target: {
336+
selectedOptions: [
337+
{ value: "option2" },
338+
],
339+
},
340+
});
341+
assert.equal(
342+
isPlainSerializableObject(result),
343+
true,
344+
"Result should be a plain serializable object",
345+
);
346+
assert.equal(
347+
"timeStamp" in (result as any),
348+
false,
349+
"Should not include timeStamp",
350+
);
351+
assert.equal(
352+
(result as any).target && "id" in (result as any).target,
353+
false,
354+
"Should not include id on target",
355+
);
231356
});
232357
});

0 commit comments

Comments
 (0)