Combobox: Customize custom value description (#117291)

Combobox: Make custom value description customizable via prop

Add a new optional `customValueDescription` prop to Combobox and MultiCombobox components that allows customizing the description text shown for custom values. The existing "Use custom value" translation remains as the default.
This commit is contained in:
Alex Khomenko 2026-02-03 12:43:55 +02:00 committed by GitHub
parent ec0104d1ed
commit b980c80d0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 33 additions and 5 deletions

View file

@ -32,6 +32,11 @@ interface ComboboxStaticProps<T extends string | number>
* Allows the user to set a value which is not in the list of options.
*/
createCustomValue?: boolean;
/**
* Custom description text for the "create custom value" option.
* Defaults to "Use custom value".
*/
customValueDescription?: string;
/**
* Custom container for rendering the dropdown menu via Portal
*/
@ -133,6 +138,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
placeholder: placeholderProp,
isClearable, // this should be default false, but TS can't infer the conditional type if you do
createCustomValue = false,
customValueDescription,
id,
width,
minWidth,
@ -158,7 +164,7 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
updateOptions,
asyncLoading,
asyncError,
} = useOptions(props.options, createCustomValue);
} = useOptions(props.options, createCustomValue, customValueDescription);
const isAsync = typeof allOptions === 'function';
const selectedItemIndex = useMemo(() => {

View file

@ -53,6 +53,7 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
maxWidth,
isClearable,
createCustomValue = false,
customValueDescription,
'aria-labelledby': ariaLabelledBy,
'data-testid': dataTestId,
portalContainer,
@ -80,7 +81,7 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
updateOptions,
asyncLoading,
asyncError,
} = useOptions(props.options, createCustomValue);
} = useOptions(props.options, createCustomValue, customValueDescription);
const options = useMemo(() => {
// Only add the 'All' option if there's more than 1 option
const addAllOption = enableAllOption && baseOptions.length > 1;

View file

@ -69,6 +69,23 @@ describe('useOptions', () => {
]);
});
it('should use custom description when provided', () => {
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Carrot', value: 'carrot' },
];
const { result } = renderHook(() => useOptions(options, true, 'Create new item'));
act(() => {
result.current.updateOptions('car');
});
expect(result.current.options).toEqual([
{ label: 'car', value: 'car', description: 'Create new item' },
{ label: 'Carrot', value: 'carrot' },
]);
});
it('should not add a custom value if it already exists', () => {
const options = [
{ label: 'Apple', value: 'apple' },

View file

@ -27,7 +27,11 @@ export const DEBOUNCE_TIME_MS = 200;
* - function to call when user types (to filter, or call async fn)
* - loading and error states
*/
export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T>, createCustomValue: boolean) {
export function useOptions<T extends string | number>(
rawOptions: AsyncOptions<T>,
createCustomValue: boolean,
customValueDescription?: string
) {
const isAsync = typeof rawOptions === 'function';
const loadOptions = useLatestAsyncCall(isAsync ? rawOptions : asyncNoop);
@ -76,13 +80,13 @@ export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T
currentOptions.unshift({
label: userTypedSearch,
value: userTypedSearch as T,
description: t('combobox.custom-value.description', 'Use custom value'),
description: customValueDescription ?? t('combobox.custom-value.description', 'Use custom value'),
});
}
}
return currentOptions;
},
[createCustomValue, userTypedSearch]
[createCustomValue, customValueDescription, userTypedSearch]
);
const updateOptions = useCallback(