Skip to content

Commit

Permalink
feat: clearOnDeselect prop for Combobox.Input (#1118)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Feb 8, 2025
1 parent f94a72c commit 9b0840f
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-pots-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

feat: add `clearOnDeselect` prop to `Combobox.Input` to clear the input's value when the last (multiple) or only (single) item is deselected.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ref = $bindable(null),
child,
defaultValue,
clearOnDeselect = false,
...restProps
}: ComboboxInputProps = $props();
Expand All @@ -19,6 +20,7 @@
() => ref,
(v) => (ref = v)
),
clearOnDeselect: box.with(() => clearOnDeselect),
});
if (defaultValue) {
Expand Down
7 changes: 7 additions & 0 deletions packages/bits-ui/src/lib/bits/combobox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export type ComboboxInputPropsWithoutHTML = WithChild<{
* the input when the combobox is first mounted if there is already a value set.
*/
defaultValue?: string;

/**
* Whether to clear the input when the last item is deselected.
*
* @default false
*/
clearOnDeselect?: boolean;
}>;

export type ComboboxInputProps = ComboboxInputPropsWithoutHTML &
Expand Down
19 changes: 18 additions & 1 deletion packages/bits-ui/src/lib/bits/select/select.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ class SelectMultipleRootState extends SelectBaseRootState {

type SelectRootState = SelectSingleRootState | SelectMultipleRootState;

type SelectInputStateProps = WithRefProps;
type SelectInputStateProps = WithRefProps &
ReadableBoxedValues<{
clearOnDeselect: boolean;
}>;

class SelectInputState {
constructor(
Expand All @@ -280,6 +283,20 @@ class SelectInputState {

this.onkeydown = this.onkeydown.bind(this);
this.oninput = this.oninput.bind(this);

watch(
[() => this.root.opts.value.current, () => this.opts.clearOnDeselect.current],
([value, clearOnDeselect], [prevValue]) => {
if (!clearOnDeselect) return;
if (Array.isArray(value) && Array.isArray(prevValue)) {
if (value.length === 0 && prevValue.length !== 0) {
this.root.inputValue = "";
}
} else if (value === "" && prevValue !== "") {
this.root.inputValue = "";
}
}
);
}

onkeydown(e: BitsKeyboardEvent) {
Expand Down
53 changes: 52 additions & 1 deletion packages/tests/src/tests/combobox/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe("combobox - single", () => {
await openSingle({}, key);
});

it("should applie the appropriate `aria-labelledby` attribute to the group", async () => {
it("should apply the appropriate `aria-labelledby` attribute to the group", async () => {
const { group, groupHeading } = await openSingle();

expect(group).toHaveAttribute("aria-labelledby", groupHeading.id);
Expand Down Expand Up @@ -446,6 +446,25 @@ describe("combobox - single", () => {
const [item0v3] = getItems(getByTestId);
expectSelected(item0v3!);
});

it("should clear the input when the selected item is deselected when `clearOnDeselect` is `true`", async () => {
const { getByTestId, user, trigger, input } = await openSingle({
inputProps: {
clearOnDeselect: true,
},
});

const [item0] = getItems(getByTestId);
await user.click(item0!);
expectSelected(item0!);
expect(input).toHaveValue("A");
await user.click(trigger);
const [item0v2] = getItems(getByTestId);
await user.click(item0v2!);

expect(input).toHaveValue("");
expect(input).not.toHaveValue("A");
});
});

////////////////////////////////////
Expand Down Expand Up @@ -717,6 +736,38 @@ describe("combobox - multiple", () => {
await user.click(submit);
expect(submittedValues).toHaveLength(0);
});

it("should clear the input when the last item is deselected when `clearOnDeselect` is `true`", async () => {
const { getByTestId, user, queryByTestId, input, getHiddenInputs } = await openMultiple({
inputProps: {
clearOnDeselect: true,
},
});
const [item, item2, item3] = getItems(getByTestId);
await waitFor(() => expect(queryByTestId("1-indicator")).toBeNull());
await user.click(item!);
expect(input).toHaveValue("A");
expect(getHiddenInputs()).toHaveLength(1);
expect(getHiddenInputs()[0]).toHaveValue("1");
await user.click(input);

expectSelected(item!);
await waitFor(() => expect(queryByTestId("1-indicator")).not.toBeNull());
await user.click(item);
expect(input).toHaveValue("");
expect(input).not.toHaveValue("A");
await user.click(item2);
await user.click(item3);
expect(getHiddenInputs()).toHaveLength(2);
expect(getHiddenInputs()[0]).toHaveValue("2");
expect(getHiddenInputs()[1]).toHaveValue("3");
await user.click(item3);
expect(getHiddenInputs()).toHaveLength(1);
expect(getHiddenInputs()[0]).toHaveValue("2");
await user.click(item2);
expect(input).toHaveValue("");
expect(getHiddenInputs()).toHaveLength(0);
});
});

function getItems(getter: AnyFn, items = testItems) {
Expand Down
5 changes: 5 additions & 0 deletions sites/docs/src/lib/content/api-reference/combobox.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const root = createApiSchema<ComboboxRootPropsWithoutHTML>({
description:
"Whether or not the user can deselect the selected item by pressing it in a single select.",
}),

items: createPropSchema({
type: {
type: "array",
Expand Down Expand Up @@ -280,6 +281,10 @@ export const input = createApiSchema<ComboboxInputPropsWithoutHTML>({
description:
"The default value of the input. This is not a reactive prop and is only used to populate the input when the combobox is first mounted if there is already a value set.",
}),
clearOnDeselect: createBooleanProp({
description: "Whether to clear the input when the last item is deselected.",
default: C.FALSE,
}),
...withChildProps({ elType: "HTMLInputElement" }),
},
dataAttributes: [
Expand Down

0 comments on commit 9b0840f

Please sign in to comment.