Skip to content

Commit 1a9c9c4

Browse files
committed
feat(combo-box): allow custom value (#2232)
Closes #1726
1 parent 416d9e9 commit 1a9c9c4

File tree

8 files changed

+147
-29
lines changed

8 files changed

+147
-29
lines changed

COMPONENT_INDEX.md

Lines changed: 28 additions & 27 deletions
Large diffs are not rendered by default.

docs/src/COMPONENT_API.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,18 @@
19711971
"constant": false,
19721972
"reactive": true
19731973
},
1974+
{
1975+
"name": "allowCustomValue",
1976+
"kind": "let",
1977+
"description": "Set to `true` to allow custom values that are not in the items list.\nBy default, user-entered text is cleared when the combobox loses focus without selecting an item.\nWhen enabled, custom text is preserved.",
1978+
"type": "boolean",
1979+
"value": "false",
1980+
"isFunction": false,
1981+
"isFunctionDeclaration": false,
1982+
"isRequired": false,
1983+
"constant": false,
1984+
"reactive": false
1985+
},
19741986
{
19751987
"name": "shouldFilterItem",
19761988
"kind": "let",

docs/src/pages/components/ComboBox.svx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ For async (e.g., server-side) filtering, bind to `value` and update `items` when
9191

9292
<FileSource src="/framed/ComboBox/AsyncComboBox" />
9393

94+
## Allow custom value
95+
96+
Set `allowCustomValue` to `true` to let users enter custom text that isn't in the predefined list. By default, user-entered text is cleared when the combobox loses focus without selecting an item. When `allowCustomValue` is enabled, custom text is preserved.
97+
98+
<FileSource src="/framed/ComboBox/AllowCustomValue" />
99+
94100
## Top direction
95101

96102
Set `direction` to `"top"` to make the dropdown menu appear above the input.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script>
2+
import { ComboBox } from "carbon-components-svelte";
3+
4+
let selectedId = undefined;
5+
let value = "";
6+
</script>
7+
8+
<ComboBox
9+
allowCustomValue
10+
titleText="Favorite fruit"
11+
placeholder="Select or enter a fruit"
12+
helperText="You can select from the list or type your own"
13+
bind:selectedId
14+
bind:value
15+
items={[
16+
{ id: "0", text: "Apple" },
17+
{ id: "1", text: "Banana" },
18+
{ id: "2", text: "Orange" },
19+
{ id: "3", text: "Strawberry" },
20+
]}
21+
shouldFilterItem={(item, value) => {
22+
if (!value) return true;
23+
return item.text.toLowerCase().includes(value.toLowerCase());
24+
}}
25+
on:select={(e) => {
26+
console.log("Selected item:", e.detail);
27+
}}
28+
/>
29+
30+
<div style:margin-top="var(--cds-layout-01)">
31+
<div><strong>Selected ID:</strong> {selectedId ?? "none"}</div>
32+
<div><strong>Current value:</strong> {value || "empty"}</div>
33+
</div>

src/ComboBox/ComboBox.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@
7373
/** Set to `true` to open the combobox menu dropdown */
7474
export let open = false;
7575
76+
/**
77+
* Set to `true` to allow custom values that are not in the items list.
78+
* By default, user-entered text is cleared when the combobox loses focus without selecting an item.
79+
* When enabled, custom text is preserved.
80+
*/
81+
export let allowCustomValue = false;
82+
7683
/**
7784
* Determine if an item should be filtered given the current combobox value
7885
* @type {(item: ComboBoxItem, value: string) => boolean}
@@ -177,8 +184,8 @@
177184
filteredItems = [];
178185
if (!selectedItem) {
179186
selectedId = undefined;
180-
// Only reset value if the input is not focused
181-
if (!ref.contains(document.activeElement)) {
187+
// Only reset value if the input is not focused and allowCustomValue is false
188+
if (!ref.contains(document.activeElement) && !allowCustomValue) {
182189
value = "";
183190
}
184191
highlightedIndex = -1;

tests/ComboBox/ComboBox.test.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
) => item.text.toLowerCase().includes(value.toLowerCase());
2828
export let translateWithIdSelection: ComponentProps<ComboBox>["translateWithIdSelection"] =
2929
undefined;
30+
export let allowCustomValue = false;
3031
</script>
3132

3233
<ComboBox
@@ -47,6 +48,7 @@
4748
{warnText}
4849
{shouldFilterItem}
4950
{translateWithIdSelection}
51+
{allowCustomValue}
5052
on:select={(e) => {
5153
console.log("select", e.detail);
5254
}}

tests/ComboBox/ComboBox.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,53 @@ describe("ComboBox", () => {
421421
const dropdown = screen.queryAllByRole("listbox")[1];
422422
expect(dropdown).toBeUndefined();
423423
});
424+
425+
it("should preserve custom value when allowCustomValue is true and user clicks away", async () => {
426+
render(ComboBox, { props: { allowCustomValue: true } });
427+
428+
const input = getInput();
429+
await user.click(input);
430+
await user.type(input, "Custom Value");
431+
await user.click(document.body);
432+
expect(input).toHaveValue("Custom Value");
433+
});
434+
435+
it("should preserve custom value when allowCustomValue is true and menu closes", async () => {
436+
render(ComboBox, { props: { allowCustomValue: true } });
437+
438+
const input = getInput();
439+
await user.click(input);
440+
await user.type(input, "My Custom Text");
441+
await user.keyboard("{Tab}");
442+
expect(input).toHaveValue("My Custom Text");
443+
});
444+
445+
it("should clear custom value when allowCustomValue is false (default behavior)", async () => {
446+
render(ComboBox);
447+
448+
const input = getInput();
449+
await user.click(input);
450+
await user.type(input, "Custom Value");
451+
await user.click(document.body);
452+
expect(input).toHaveValue("");
453+
});
454+
455+
it("should preserve custom value when allowCustomValue is true and Enter is pressed", async () => {
456+
render(ComboBox, { props: { allowCustomValue: true } });
457+
458+
const input = getInput();
459+
await user.click(input);
460+
await user.type(input, "New Custom Entry");
461+
await user.keyboard("{Enter}");
462+
expect(input).toHaveValue("New Custom Entry");
463+
});
464+
465+
it("should still allow selecting items from list when allowCustomValue is true", async () => {
466+
render(ComboBox, { props: { allowCustomValue: true } });
467+
468+
const input = getInput();
469+
await user.click(input);
470+
await user.click(screen.getByText("Email"));
471+
expect(input).toHaveValue("Email");
472+
});
424473
});

types/ComboBox/ComboBox.svelte.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ type $Props = {
114114
*/
115115
open?: boolean;
116116

117+
/**
118+
* Set to `true` to allow custom values that are not in the items list.
119+
* By default, user-entered text is cleared when the combobox loses focus without selecting an item.
120+
* When enabled, custom text is preserved.
121+
* @default false
122+
*/
123+
allowCustomValue?: boolean;
124+
117125
/**
118126
* Determine if an item should be filtered given the current combobox value
119127
* @default () => true

0 commit comments

Comments
 (0)