-
Notifications
You must be signed in to change notification settings - Fork 36
Allow custom values in Combobox and DynamicCombobox #948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
17089be
b32c393
25b158e
13440d9
a7ff475
be8bff9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@saleor/macaw-ui": minor | ||
| --- | ||
|
|
||
| Combobox and DynamicCombobox now accepts new props that allow entering custom values | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DynamicCombobox> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const AllowCustomValue = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [options, setOptions] = useState<Option[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [value, setValue] = useState<Option | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleInputValueChange = async (criteria: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!criteria) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOptions([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `https://swapi.dev/api/people/?search=${criteria}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOptions( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.results.map((result: { name: string }) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: result.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: result.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+190
to
+205
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | |
| } | |
| setLoading(true); | |
| const response = await fetch( | |
| `https://swapi.dev/api/people/?search=${criteria}` | |
| ); | |
| const body = await response.json(); | |
| setOptions( | |
| body.results.map((result: { name: string }) => ({ | |
| value: result.name, | |
| label: result.name, | |
| })) | |
| ); | |
| setLoading(false); | |
| setLoading(false); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| const response = await fetch( | |
| `https://swapi.dev/api/people/?search=${criteria}` | |
| ); | |
| const body = await response.json(); | |
| setOptions( | |
| body.results.map((result: { name: string }) => ({ | |
| value: result.name, | |
| label: result.name, | |
| })) | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } |
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description mentions an AllowCustomValueWithLocale story for the dynamic combobox as well, but this file only adds AllowCustomValue. Either add the locale variant story here (to exercise locale.addNewLabel) or update the PR description/test plan to match what’s included.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,7 +26,8 @@ import { | |||||||||||||
| SingleChangeHandler, | ||||||||||||||
| } from "../../BaseSelect"; | ||||||||||||||
|
|
||||||||||||||
| import { ComboboxWrapper, useCombobox } from "../Common"; | ||||||||||||||
| import { ComboboxWrapper } from "../Common"; | ||||||||||||||
| import { isCustomValueOption, useCombobox } from "../Common/useCombobox"; | ||||||||||||||
|
|
||||||||||||||
| export type DynamicComboboxProps<T> = PropsWithBox< | ||||||||||||||
| Omit< | ||||||||||||||
|
|
@@ -54,8 +55,11 @@ export type DynamicComboboxProps<T> = PropsWithBox< | |||||||||||||
| children?: ReactNode; | ||||||||||||||
| locale?: { | ||||||||||||||
| loadingText?: string; | ||||||||||||||
| addNewLabel?: string; | ||||||||||||||
| }; | ||||||||||||||
| onScrollEnd?: () => void; | ||||||||||||||
| allowCustomValue?: boolean; | ||||||||||||||
| onCustomValueSubmit?: (value: string) => void; | ||||||||||||||
| } | ||||||||||||||
| > & | ||||||||||||||
| InputVariants; | ||||||||||||||
|
|
@@ -81,6 +85,8 @@ const DynamicComboboxInner = <T extends Option>( | |||||||||||||
| startAdornment, | ||||||||||||||
| endAdornment, | ||||||||||||||
| onScrollEnd, | ||||||||||||||
| allowCustomValue, | ||||||||||||||
| onCustomValueSubmit, | ||||||||||||||
| ...props | ||||||||||||||
| }: DynamicComboboxProps<T>, | ||||||||||||||
| ref: ForwardedRef<HTMLInputElement> | ||||||||||||||
|
|
@@ -97,6 +103,7 @@ const DynamicComboboxInner = <T extends Option>( | |||||||||||||
| getItemProps, | ||||||||||||||
| itemsToSelect, | ||||||||||||||
| hasItemsToSelect, | ||||||||||||||
| inputValue, | ||||||||||||||
| } = useCombobox({ | ||||||||||||||
| selectedItem: value, | ||||||||||||||
| options, | ||||||||||||||
|
|
@@ -105,6 +112,8 @@ const DynamicComboboxInner = <T extends Option>( | |||||||||||||
| onInputValueChange, | ||||||||||||||
| onFocus, | ||||||||||||||
| onBlur, | ||||||||||||||
| allowCustomValue, | ||||||||||||||
lkostrowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
| onCustomValueSubmit, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| const { refs, floatingStyles } = useFloating<HTMLLabelElement>({ | ||||||||||||||
|
|
@@ -170,28 +179,43 @@ const DynamicComboboxInner = <T extends Option>( | |||||||||||||
| {...getMenuProps({ ref: refs.floating })} | ||||||||||||||
| > | ||||||||||||||
| {isOpen && | ||||||||||||||
| itemsToSelect?.map((item, index) => ( | ||||||||||||||
| <List.Item | ||||||||||||||
| data-test-id="select-option" | ||||||||||||||
| key={`${id}-${item.value}-${index}-${highlightedIndex}`} | ||||||||||||||
| className={listItemStyle} | ||||||||||||||
| {...getItemProps({ | ||||||||||||||
| item, | ||||||||||||||
| index, | ||||||||||||||
| disabled: item.disabled, | ||||||||||||||
| })} | ||||||||||||||
| active={highlightedIndex === index} | ||||||||||||||
| > | ||||||||||||||
| {item?.startAdornment} | ||||||||||||||
| <Text | ||||||||||||||
| color={item.disabled ? "defaultDisabled" : undefined} | ||||||||||||||
| size={getListTextSize(size)} | ||||||||||||||
| itemsToSelect?.map((item, index) => | ||||||||||||||
| isCustomValueOption(item) ? ( | ||||||||||||||
| <List.Item | ||||||||||||||
| data-test-id="combobox-custom-value" | ||||||||||||||
| key={`${id}-custom-value`} | ||||||||||||||
| className={listItemStyle} | ||||||||||||||
| {...getItemProps({ item, index })} | ||||||||||||||
| active={highlightedIndex === index} | ||||||||||||||
| cursor="pointer" | ||||||||||||||
| > | ||||||||||||||
| {item.label} | ||||||||||||||
| </Text> | ||||||||||||||
| {item?.endAdornment} | ||||||||||||||
| </List.Item> | ||||||||||||||
| ))} | ||||||||||||||
| <Text size={getListTextSize(size)}> | ||||||||||||||
| {locale?.addNewLabel ?? "Add new"}: {inputValue} | ||||||||||||||
| </Text> | ||||||||||||||
| </List.Item> | ||||||||||||||
| ) : ( | ||||||||||||||
| <List.Item | ||||||||||||||
| data-test-id="select-option" | ||||||||||||||
| key={`${id}-${item.value}-${index}-${highlightedIndex}`} | ||||||||||||||
| className={listItemStyle} | ||||||||||||||
| {...getItemProps({ | ||||||||||||||
| item, | ||||||||||||||
| index, | ||||||||||||||
| disabled: item.disabled, | ||||||||||||||
| })} | ||||||||||||||
| active={highlightedIndex === index} | ||||||||||||||
| > | ||||||||||||||
| {item?.startAdornment} | ||||||||||||||
| <Text | ||||||||||||||
| color={item.disabled ? "defaultDisabled" : undefined} | ||||||||||||||
| size={getListTextSize(size)} | ||||||||||||||
| > | ||||||||||||||
| {item.label} | ||||||||||||||
| </Text> | ||||||||||||||
| {item?.endAdornment} | ||||||||||||||
| </List.Item> | ||||||||||||||
| ) | ||||||||||||||
| )} | ||||||||||||||
|
|
||||||||||||||
| {isOpen && !loading && !hasItemsToSelect && children} | ||||||||||||||
|
||||||||||||||
| {isOpen && !loading && !hasItemsToSelect && children} | |
| {isOpen && | |
| !loading && | |
| !hasItemsToSelect && | |
| !hasCustomValueToSubmit && | |
| children} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar: “Combobox and DynamicCombobox now accepts …” should use plural verb (“now accept …”).