diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index bded7736..dec083b9 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') {