From 17089beaec28043077a875381b7ca9422b062996 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 3 Mar 2026 12:10:50 +0100 Subject: [PATCH 1/4] combobox custom value --- .changeset/pretty-foxes-pull.md | 5 +++ .../Combobox/Common/useCombobox.tsx | 30 ++++++++++++- .../Dynamic/DynamicCombobox.stories.tsx | 44 +++++++++++++++++++ .../Combobox/Dynamic/DynamicCombobox.tsx | 25 ++++++++++- src/components/Combobox/Static/Combobox.tsx | 28 +++++++++++- .../Static/StaticCombobox.stories.tsx | 18 ++++++++ 6 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 .changeset/pretty-foxes-pull.md diff --git a/.changeset/pretty-foxes-pull.md b/.changeset/pretty-foxes-pull.md new file mode 100644 index 000000000..126e54a8c --- /dev/null +++ b/.changeset/pretty-foxes-pull.md @@ -0,0 +1,5 @@ +--- +"@saleor/macaw-ui": minor +--- + +Combobox and DynamicCombobox now accepts new props that allow entering custom values diff --git a/src/components/Combobox/Common/useCombobox.tsx b/src/components/Combobox/Common/useCombobox.tsx index 892ebbc81..58c2497d6 100644 --- a/src/components/Combobox/Common/useCombobox.tsx +++ b/src/components/Combobox/Common/useCombobox.tsx @@ -3,7 +3,7 @@ import { useCombobox as useDownshiftCombobox, UseComboboxGetInputPropsOptions, } from "downshift"; -import { FocusEvent, useState } from "react"; +import { FocusEvent, useState, KeyboardEvent } from "react"; import { Option, @@ -34,6 +34,8 @@ export const useCombobox = ({ onInputValueChange, onFocus, onBlur, + allowCustomValue, + onCustomValueSubmit, }: { selectedItem: T | null | undefined; options: T[]; @@ -42,6 +44,8 @@ export const useCombobox = ({ onInputValueChange?: (value: string) => void; onFocus?: (e: FocusEvent) => void; onBlur?: (e: FocusEvent) => void; + allowCustomValue?: boolean; + onCustomValueSubmit?: (value: string) => void; }) => { const [inputValue, setInputValue] = useState(""); const [active, setActive] = useState(false); @@ -55,6 +59,7 @@ export const useCombobox = ({ const { isOpen, + closeMenu, getToggleButtonProps, getLabelProps, getMenuProps, @@ -92,9 +97,21 @@ export const useCombobox = ({ }, }); + const hasItemsToSelect = itemsToSelect.length > 0; + const trimmedInputValue = inputValue.trim(); + const hasCustomValueToSubmit = + !!allowCustomValue && !hasItemsToSelect && trimmedInputValue.length > 0; + + const handleCustomValueSubmit = (value: string) => { + onCustomValueSubmit?.(value); + closeMenu(); + setInputValue(""); + }; + return { active, itemsToSelect, + inputValue: trimmedInputValue, typed, isOpen, getToggleButtonProps, @@ -107,6 +124,7 @@ export const useCombobox = ({ _getInputProps<{ onFocus: (e: FocusEvent) => void; onBlur: (e: FocusEvent) => void; + onKeyDown: (e: KeyboardEvent) => void; }>( { onFocus: (e) => { @@ -117,12 +135,20 @@ export const useCombobox = ({ onBlur?.(e); setActive(false); }, + onKeyDown: (e) => { + if (e.key === "Enter" && hasCustomValueToSubmit) { + e.preventDefault(); + handleCustomValueSubmit(trimmedInputValue); + } + }, ...options, }, otherOptions ), highlightedIndex, getItemProps, - hasItemsToSelect: itemsToSelect.length > 0, + hasItemsToSelect, + hasCustomValueToSubmit, + handleCustomValueSubmit, }; }; diff --git a/src/components/Combobox/Dynamic/DynamicCombobox.stories.tsx b/src/components/Combobox/Dynamic/DynamicCombobox.stories.tsx index 26327a7f0..0f30db71c 100644 --- a/src/components/Combobox/Dynamic/DynamicCombobox.stories.tsx +++ b/src/components/Combobox/Dynamic/DynamicCombobox.stories.tsx @@ -1,4 +1,5 @@ import { Meta } from "@storybook/react"; +import { fn } from "@storybook/test"; import { useState } from "react"; import { DynamicCombobox } from ".."; @@ -177,3 +178,46 @@ export const NoOptions = () => { ); }; + +export const AllowCustomValue = () => { + const [options, setOptions] = useState([]); + const [value, setValue] = useState