New Select: Initial scaffolding (#89114)

* Initial scaffolding

* Extend props from Input

* Rename to Combobox

* Use search icon

* Remove use of SelectableValue

* Remove unused import

* Memoize
This commit is contained in:
Tobias Skarhed
2024-06-13 13:41:14 +02:00
committed by GitHub
parent afcb5a855c
commit 5e2f08de31
4 changed files with 137 additions and 2 deletions
+1
View File
@@ -66,6 +66,7 @@
"classnames": "2.5.1",
"d3": "7.9.0",
"date-fns": "3.6.0",
"downshift": "^9.0.6",
"hoist-non-react-statics": "3.3.2",
"i18next": "^23.0.0",
"i18next-browser-languagedetector": "^7.0.2",
@@ -0,0 +1,47 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react';
import React, { useState } from 'react';
import { Combobox } from './Combobox';
const meta: Meta<typeof Combobox> = {
title: 'Forms/Combobox',
component: Combobox,
args: {
loading: undefined,
invalid: undefined,
placeholder: 'Select an option...',
options: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Carrot', value: 'carrot' },
{ label: 'Dill', value: 'dill' },
{ label: 'Eggplant', value: 'eggplant' },
{ label: 'Fennel', value: 'fennel' },
{ label: 'Grape', value: 'grape' },
{ label: 'Honeydew', value: 'honeydew' },
{ label: 'Iceberg Lettuce', value: 'iceberg-lettuce' },
{ label: 'Jackfruit', value: 'jackfruit' },
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: 3 },
],
value: 'banana',
},
};
export const Basic: StoryFn<typeof Combobox> = (args) => {
const [value, setValue] = useState(args.value);
return (
<Combobox
{...args}
value={value}
onChange={(val) => {
setValue(val.value);
action('onChange')(val);
}}
/>
);
};
export default meta;
@@ -0,0 +1,64 @@
import { useCombobox } from 'downshift';
import React, { useMemo, useState } from 'react';
import { Icon } from '../Icon/Icon';
import { Input, Props as InputProps } from '../Input/Input';
type Value = string | number;
type Option = {
label: string;
value: Value;
};
interface ComboboxProps
extends Omit<InputProps, 'width' | 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange'> {
onChange: (val: Option) => void;
value: Value;
options: Option[];
}
function itemToString(item: Option | null) {
return item?.label || '';
}
function itemFilter(inputValue: string) {
const lowerCasedInputValue = inputValue.toLowerCase();
return (item: Option) => {
return (
!inputValue ||
item?.label?.toLowerCase().includes(lowerCasedInputValue) ||
item?.value?.toString().toLowerCase().includes(lowerCasedInputValue)
);
};
}
export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => {
const [items, setItems] = useState(options);
const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]);
const { getInputProps, getMenuProps, getItemProps, isOpen } = useCombobox({
items,
itemToString,
selectedItem,
onInputValueChange: ({ inputValue }) => {
setItems(options.filter(itemFilter(inputValue)));
},
onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem),
});
return (
<div>
<Input suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />} {...restProps} {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen &&
items.map((item, index) => {
return (
<li key={item.value} {...getItemProps({ item, index })}>
{item.label}
</li>
);
})}
</ul>
</div>
);
};