New Select: Create custom value (#92652)
* Display custom value in option list * Ignore create new value when there is an exact match * Support displaying custom value * Add i18n string * Update failing test
This commit is contained in:
@@ -97,9 +97,9 @@ const ManyOptionsStory: StoryFn<PropsAndCustomArgs> = ({ numberOfOptions, ...arg
|
||||
loading={isLoading}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
setValue(val?.value || null);
|
||||
action('onChange')(val);
|
||||
onChange={(opt) => {
|
||||
setValue(opt?.value || null);
|
||||
action('onChange')(opt);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('Combobox', () => {
|
||||
});
|
||||
|
||||
it('clears selected value', async () => {
|
||||
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} />);
|
||||
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} isClearable />);
|
||||
|
||||
expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument();
|
||||
const input = screen.getByRole('combobox');
|
||||
|
||||
@@ -23,10 +23,12 @@ interface ComboboxProps
|
||||
onChange: (val: Option | null) => void;
|
||||
value: Value | null;
|
||||
options: Option[];
|
||||
isClearable?: boolean;
|
||||
createCustomValue?: boolean;
|
||||
}
|
||||
|
||||
function itemToString(item: Option | null) {
|
||||
return item?.label ?? '';
|
||||
return item?.label ?? item?.value?.toString() ?? '';
|
||||
}
|
||||
|
||||
function itemFilter(inputValue: string) {
|
||||
@@ -51,13 +53,44 @@ const INDEX_WIDTH_CALCULATION = 100;
|
||||
// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated.
|
||||
const WIDTH_MULTIPLIER = 7.3;
|
||||
|
||||
export const Combobox = ({ options, onChange, value, id, ...restProps }: ComboboxProps) => {
|
||||
export const Combobox = ({
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
isClearable = false,
|
||||
createCustomValue = false,
|
||||
id,
|
||||
...restProps
|
||||
}: ComboboxProps) => {
|
||||
const [items, setItems] = useState(options);
|
||||
const selectedItemIndex = useMemo(
|
||||
() => options.findIndex((option) => option.value === value) || null,
|
||||
[options, value]
|
||||
);
|
||||
const selectedItem = selectedItemIndex ? options[selectedItemIndex] : null;
|
||||
|
||||
const selectedItemIndex = useMemo(() => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = options.findIndex((option) => option.value === value);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return index;
|
||||
}, [options, value]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
if (selectedItemIndex) {
|
||||
return options[selectedItemIndex];
|
||||
}
|
||||
|
||||
// Custom value
|
||||
if (value !== null) {
|
||||
return {
|
||||
label: value.toString(),
|
||||
value,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [selectedItemIndex, options, value]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const floatingRef = useRef<HTMLDivElement>(null);
|
||||
@@ -82,18 +115,34 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
setInputValue,
|
||||
selectItem,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
selectItem,
|
||||
} = useCombobox({
|
||||
inputId: id,
|
||||
items,
|
||||
itemToString,
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem, inputValue }) => {
|
||||
onChange(selectedItem);
|
||||
},
|
||||
defaultHighlightedIndex: selectedItemIndex ?? undefined,
|
||||
scrollIntoView: () => {},
|
||||
onInputValueChange: ({ inputValue }) => {
|
||||
setItems(options.filter(itemFilter(inputValue)));
|
||||
const filteredItems = options.filter(itemFilter(inputValue));
|
||||
if (createCustomValue && inputValue && filteredItems.findIndex((opt) => opt.label === inputValue) === -1) {
|
||||
setItems([
|
||||
...filteredItems,
|
||||
{
|
||||
label: inputValue,
|
||||
value: inputValue,
|
||||
description: t('combobox.custom-value.create', 'Create custom value'),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
} else {
|
||||
setItems(filteredItems);
|
||||
}
|
||||
},
|
||||
onIsOpenChange: ({ isOpen }) => {
|
||||
// Default to displaying all values when opening
|
||||
@@ -102,9 +151,6 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
return;
|
||||
}
|
||||
},
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
onChange(selectedItem);
|
||||
},
|
||||
onHighlightedIndexChange: ({ highlightedIndex, type }) => {
|
||||
if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) {
|
||||
rowVirtualizer.scrollToIndex(highlightedIndex);
|
||||
@@ -113,8 +159,8 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
});
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setInputValue(selectedItem?.label ?? '');
|
||||
}, [selectedItem, setInputValue]);
|
||||
setInputValue(selectedItem?.label ?? value?.toString() ?? '');
|
||||
}, [selectedItem, setInputValue, value]);
|
||||
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
@@ -147,7 +193,7 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
<Input
|
||||
suffix={
|
||||
<>
|
||||
{!!value && value === selectedItem?.value && (
|
||||
{!!value && value === selectedItem?.value && isClearable && (
|
||||
<Icon
|
||||
name="times"
|
||||
className={styles.clear}
|
||||
@@ -202,7 +248,7 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
return (
|
||||
<li
|
||||
key={items[virtualRow.index].value}
|
||||
key={items[virtualRow.index].value + items[virtualRow.index].label}
|
||||
data-index={virtualRow.index}
|
||||
className={cx(
|
||||
styles.option,
|
||||
|
||||
@@ -333,6 +333,9 @@
|
||||
"combobox": {
|
||||
"clear": {
|
||||
"title": "Clear value"
|
||||
},
|
||||
"custom-value": {
|
||||
"create": "Create custom value"
|
||||
}
|
||||
},
|
||||
"command-palette": {
|
||||
|
||||
@@ -333,6 +333,9 @@
|
||||
"combobox": {
|
||||
"clear": {
|
||||
"title": "Cľęäř väľūę"
|
||||
},
|
||||
"custom-value": {
|
||||
"create": "Cřęäŧę čūşŧőm väľūę"
|
||||
}
|
||||
},
|
||||
"command-palette": {
|
||||
|
||||
Reference in New Issue
Block a user