From 5e2f08de316dab68a25e251bf7b6eb80c01774c0 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:41:14 +0200 Subject: [PATCH] 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 --- packages/grafana-ui/package.json | 1 + .../Combobox/Combobox.internal.story.tsx | 47 ++++++++++++++ .../src/components/Combobox/Combobox.tsx | 64 +++++++++++++++++++ yarn.lock | 27 +++++++- 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx create mode 100644 packages/grafana-ui/src/components/Combobox/Combobox.tsx diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index f497b997252..78a915e534b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx new file mode 100644 index 00000000000..e56a989a526 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx @@ -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 = { + 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 = (args) => { + const [value, setValue] = useState(args.value); + return ( + { + setValue(val.value); + action('onChange')(val); + }} + /> + ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx new file mode 100644 index 00000000000..ff83f3edc27 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -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 { + 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 ( +
+ } {...restProps} {...getInputProps()} /> +
    + {isOpen && + items.map((item, index) => { + return ( +
  • + {item.label} +
  • + ); + })} +
+
+ ); +}; diff --git a/yarn.lock b/yarn.lock index a83d5997e87..6be3c8126af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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