Skip to content

Commit 64561c6

Browse files
authored
fix(Checkbox): ensure Checkbox.Group value setter is called (#1440)
1 parent 8b88c8b commit 64561c6

File tree

6 files changed

+71
-10
lines changed

6 files changed

+71
-10
lines changed

.changeset/silly-months-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix(Checkbox): ensure Checkbox.Group value setter is called

packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,19 @@ class CheckboxGroupState {
3737
addValue(checkboxValue: string | undefined) {
3838
if (!checkboxValue) return;
3939
if (!this.opts.value.current.includes(checkboxValue)) {
40-
this.opts.value.current.push(checkboxValue);
41-
this.opts.onValueChange.current(this.opts.value.current);
40+
const newValue = [...$state.snapshot(this.opts.value.current), checkboxValue];
41+
this.opts.value.current = newValue;
42+
this.opts.onValueChange.current(newValue);
4243
}
4344
}
4445

4546
removeValue(checkboxValue: string | undefined) {
4647
if (!checkboxValue) return;
4748
const index = this.opts.value.current.indexOf(checkboxValue);
4849
if (index === -1) return;
49-
this.opts.value.current.splice(index, 1);
50-
this.opts.onValueChange.current(this.opts.value.current);
50+
const newValue = this.opts.value.current.filter((v) => v !== checkboxValue);
51+
this.opts.value.current = newValue;
52+
this.opts.onValueChange.current(newValue);
5153
}
5254

5355
props = $derived.by(

packages/bits-ui/src/lib/bits/checkbox/components/checkbox-group.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
required: box.with(() => Boolean(required)),
2929
name: box.with(() => name),
3030
value: box.with(
31-
() => value,
31+
() => $state.snapshot(value),
3232
(v) => {
33-
value = v;
33+
value = $state.snapshot(v);
3434
onValueChange(v);
3535
}
3636
),

packages/bits-ui/src/lib/bits/checkbox/components/checkbox.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { CheckboxGroupContext, useCheckboxRoot } from "../checkbox.svelte.js";
55
import CheckboxInput from "./checkbox-input.svelte";
66
import { useId } from "$lib/internal/use-id.js";
7+
import { watch } from "runed";
78
89
let {
910
checked = $bindable(false),
@@ -32,6 +33,19 @@
3233
}
3334
}
3435
36+
watch.pre(
37+
() => value,
38+
() => {
39+
if (group && value) {
40+
if (group.opts.value.current.includes(value)) {
41+
checked = true;
42+
} else {
43+
checked = false;
44+
}
45+
}
46+
}
47+
);
48+
3549
const rootState = useCheckboxRoot(
3650
{
3751
checked: box.with(

tests/src/tests/checkbox/checkbox-group-test.svelte

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
import { Checkbox } from "bits-ui";
33
44
let {
5-
value = $bindable([]),
5+
value: valueProp = $bindable([]),
66
items = [],
77
disabledItems = [],
88
onFormSubmit,
9+
getValue: getValueProp,
10+
setValue: setValueProp,
911
...restProps
1012
}: Checkbox.GroupProps & {
1113
/**
@@ -14,7 +16,11 @@
1416
items?: string[];
1517
disabledItems?: string[];
1618
onFormSubmit?: (fd: FormData) => void;
19+
setValue?: (value: string[]) => void;
20+
getValue?: () => string[];
1721
} = $props();
22+
23+
let myValue = $state(valueProp);
1824
</script>
1925

2026
{#snippet MyCheckbox({ itemValue }: { itemValue: string })}
@@ -44,14 +50,29 @@
4450
onFormSubmit?.(formData);
4551
}}
4652
>
47-
<p data-testid="binding">{value}</p>
48-
<Checkbox.Group data-testid="group" bind:value {...restProps}>
53+
<p data-testid="binding">{myValue}</p>
54+
<Checkbox.Group
55+
data-testid="group"
56+
bind:value={
57+
() => {
58+
getValueProp?.();
59+
return myValue;
60+
},
61+
(v) => {
62+
setValueProp?.(v);
63+
myValue = v;
64+
}
65+
}
66+
{...restProps}
67+
>
4968
<Checkbox.GroupLabel data-testid="group-label">My Group</Checkbox.GroupLabel>
5069
{#each items as itemValue}
5170
{@render MyCheckbox({ itemValue })}
5271
{/each}
5372
</Checkbox.Group>
5473
<button type="submit" data-testid="submit"> Submit </button>
5574
</form>
56-
<button data-testid="update" onclick={() => (value = ["c", "d"])}> Programmatic update </button>
75+
<button data-testid="update" onclick={() => (myValue = ["c", "d"])}>
76+
Programmatic update
77+
</button>
5778
</main>

tests/src/tests/checkbox/checkbox.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,28 @@ describe("Checkbox Group", () => {
256256
expect(t.binding).toHaveTextContent("b,d");
257257
});
258258

259+
it("should handle function binding", async () => {
260+
const setMock = vi.fn();
261+
const getMock = vi.fn();
262+
const t = setupGroup({ getValue: getMock, setValue: setMock, value: [] });
263+
const [a, b, _, d] = t.checkboxes;
264+
await t.user.click(a);
265+
expect(setMock).toHaveBeenCalledWith(["a"]);
266+
setMock.mockClear();
267+
await t.user.click(b);
268+
expect(setMock).toHaveBeenCalledWith(["a", "b"]);
269+
setMock.mockClear();
270+
await t.user.click(d);
271+
expect(setMock).toHaveBeenCalledWith(["a", "b", "d"]);
272+
setMock.mockClear();
273+
await t.user.click(a);
274+
expect(setMock).toHaveBeenCalledWith(["b", "d"]);
275+
});
276+
259277
it("should handle programmatic value changes", async () => {
260278
const t = setupGroup({ value: ["a", "b"] });
261279
const [a, b, c, d] = t.checkboxes;
280+
await tick();
262281
expectChecked(a, b);
263282
await t.user.click(t.updateBtn);
264283
expectUnchecked(a, b);

0 commit comments

Comments
 (0)