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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 137 additions and 2 deletions

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",

View File

@ -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;

View File

@ -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>
);
};

View File

@ -3684,6 +3684,7 @@ __metadata:
csstype: "npm:3.1.3"
d3: "npm:7.9.0"
date-fns: "npm:3.6.0"
downshift: "npm:^9.0.6"
esbuild: "npm:0.20.2"
expose-loader: "npm:5.0.0"
hoist-non-react-statics: "npm:3.3.2"
@ -12460,6 +12461,13 @@ __metadata:
languageName: node
linkType: hard
"compute-scroll-into-view@npm:^3.1.0":
version: 3.1.0
resolution: "compute-scroll-into-view@npm:3.1.0"
checksum: 10/cc5211d49bced5ad23385da5c2eaf69b6045628581b0dcb9f4dd407bfee51bbd26d2bce426be26edf2feaf8c243706f5a7c3759827d89cc5a01a5cf7d299a5eb
languageName: node
linkType: hard
"concat-map@npm:0.0.1":
version: 0.0.1
resolution: "concat-map@npm:0.0.1"
@ -14380,6 +14388,21 @@ __metadata:
languageName: node
linkType: hard
"downshift@npm:^9.0.6":
version: 9.0.6
resolution: "downshift@npm:9.0.6"
dependencies:
"@babel/runtime": "npm:^7.24.5"
compute-scroll-into-view: "npm:^3.1.0"
prop-types: "npm:^15.8.1"
react-is: "npm:18.2.0"
tslib: "npm:^2.6.2"
peerDependencies:
react: ">=16.12.0"
checksum: 10/e84ceba61429694395e6c2ab7213e76d6807a87ceb52b0db08642120ac6d6affc74426772431df29741d571743143316a11e6a815280bed4bbc1113cd83849b1
languageName: node
linkType: hard
"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2":
version: 0.1.2
resolution: "duplexer@npm:0.1.2"
@ -25292,7 +25315,7 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0":
"react-is@npm:18.2.0, react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df
@ -29156,7 +29179,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.6.3, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1":
"tslib@npm:2.6.3, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.6.2":
version: 2.6.3
resolution: "tslib@npm:2.6.3"
checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c