diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json
index 664ad168..7df10aa9 100644
--- a/docs/src/COMPONENT_API.json
+++ b/docs/src/COMPONENT_API.json
@@ -1505,6 +1505,18 @@
"constant": false,
"reactive": true
},
+ {
+ "name": "allowArbitraryValues",
+ "kind": "let",
+ "description": "Set to `true` to allow values that aren't included in `items`",
+ "type": "boolean",
+ "value": "false",
+ "isFunction": false,
+ "isFunctionDeclaration": false,
+ "isRequired": false,
+ "constant": false,
+ "reactive": false
+ },
{
"name": "direction",
"kind": "let",
diff --git a/docs/src/pages/components/ComboBox.svx b/docs/src/pages/components/ComboBox.svx
index 559b4127..4552cd15 100644
--- a/docs/src/pages/components/ComboBox.svx
+++ b/docs/src/pages/components/ComboBox.svx
@@ -34,6 +34,19 @@ items={[
{id: "2", text: "Fax"}
]} />
+## Arbitrary Values
+
+Set `allowArbitraryValues` to `true` to allow the user to type in whatever value they want, even if it's not present in `items`.
+`selectedId` will be set to `undefined` when the value is arbitrary.
+
+
+
## Reactive example
diff --git a/docs/yarn.lock b/docs/yarn.lock
index d2561d54..f08d35f9 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -120,6 +120,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz#b2da6219b603e3fa371a78f53f5361260d0c5585"
integrity sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==
+"@ibm/telemetry-js@^1.2.1":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@ibm/telemetry-js/-/telemetry-js-1.3.0.tgz#321f2ed4bbbc78d69dc1ee9cb6f83d2d2af9baef"
+ integrity sha512-9gIkyF2B9RizWN6rsdQN76DN6D+/Xbr4HGTwm6EUujfXvEVtWbf4jzxDFwKvZkeTC2tjHpkUNJQKdivbMKt8yg==
+
"@jridgewell/gen-mapping@^0.3.0":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
@@ -399,8 +404,9 @@ bufferutil@^4.0.1:
node-gyp-build "~3.7.0"
carbon-components-svelte@../:
- version "0.82.5"
+ version "0.84.1"
dependencies:
+ "@ibm/telemetry-js" "^1.2.1"
flatpickr "4.6.9"
carbon-icons-svelte@^12.4.1:
diff --git a/src/ComboBox/ComboBox.svelte b/src/ComboBox/ComboBox.svelte
index 0aee1350..1d4464d2 100644
--- a/src/ComboBox/ComboBox.svelte
+++ b/src/ComboBox/ComboBox.svelte
@@ -27,6 +27,12 @@
/** Specify the selected combobox value */
export let value = "";
+ /**
+ * Allow values that aren't in `items`.
+ * @type {boolean}
+ */
+ export let allowArbitraryValues = false
+
/**
* Specify the direction of the combobox dropdown menu
* @type {"bottom" | "top"}
@@ -107,7 +113,7 @@
*/
export let listRef = null;
- import { createEventDispatcher, afterUpdate, tick } from "svelte";
+ import { createEventDispatcher, afterUpdate, tick, onMount } from "svelte";
import Checkmark from "../icons/Checkmark.svelte";
import WarningFilled from "../icons/WarningFilled.svelte";
import WarningAltFilled from "../icons/WarningAltFilled.svelte";
@@ -165,6 +171,12 @@
if (options?.focus !== false) ref?.focus();
}
+ onMount(() => {
+ if (selectedItem) {
+ value = itemToString(selectedItem)
+ }
+ })
+
afterUpdate(() => {
if (open) {
ref.focus();
@@ -172,14 +184,16 @@
} else {
highlightedIndex = -1;
filteredItems = [];
- if (!selectedItem) {
- selectedId = undefined;
- value = "";
- highlightedIndex = -1;
- highlightedId = undefined;
- } else {
- // programmatically set value
- value = itemToString(selectedItem);
+ if (!allowArbitraryValues) {
+ if (!selectedItem) {
+ selectedId = undefined;
+ value = "";
+ highlightedIndex = -1;
+ highlightedId = undefined;
+ } else {
+ // programmatically set value
+ value = itemToString(selectedItem);
+ }
}
}
});
@@ -202,6 +216,39 @@
selectedItem = undefined;
}
+ function searchForMatchingValue() {
+ // searching typed value in text list with lowercase
+ let matchedItem =
+ filteredItems.find(
+ (e) =>
+ e.text.toLowerCase() === value?.toLowerCase() && !e.disabled
+ );
+
+ if (!allowArbitraryValues) {
+ // typed value has matched or fallback to first enabled item
+ matchedItem = matchedItem ?? filteredItems.find((e) => !e.disabled);
+ if (matchedItem) setMatchedItem(matchedItem)
+ } else {
+ // When allowing arbitrary values, we still want to select a value if the user types in one that exists.
+ // But if it doesn't exist, we don't try to fallback to another value.
+ if (matchedItem) {
+ setMatchedItem(matchedItem)
+ } else {
+ open = false;
+ selectedItem = undefined;
+ selectedId = undefined;
+ }
+ }
+ }
+
+ /** @param item {ComboBoxItem}*/
+ function setMatchedItem(item) {
+ open = false;
+ selectedItem = item;
+ value = itemToString(selectedItem);
+ selectedId = selectedItem.id;
+ }
+
$: ariaLabel = $$props["aria-label"] || "Choose an item";
$: menuId = `menu-${id}`;
$: comboId = `combo-${id}`;
@@ -305,19 +352,7 @@
selectedId = filteredItems[highlightedIndex].id;
}
} else {
- // searching typed value in text list with lowercase
- const matchedItem =
- filteredItems.find(
- (e) =>
- e.text.toLowerCase() === value?.toLowerCase() && !e.disabled
- ) ?? filteredItems.find((e) => !e.disabled);
- if (matchedItem) {
- // typed value has matched or fallback to first enabled item
- open = false;
- selectedItem = matchedItem;
- value = itemToString(selectedItem);
- selectedId = selectedItem.id;
- }
+ searchForMatchingValue()
}
highlightedIndex = -1;
} else if (key === 'Tab') {