From 0443b7a2ab275413773f32f90e8cdeb49c7ee9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 14:50:50 -0300 Subject: [PATCH] feat: create component for tag multi select with new design (#584) --- src/components/ComboBox.tsx | 4 +- src/components/TagMultiSelect.tsx | 158 +++++++++++++++++++++++++ src/index.ts | 1 + src/stories/ComboBox.stories.tsx | 34 ++++++ src/stories/TagMultiselectTemplate.tsx | 28 +++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/components/TagMultiSelect.tsx create mode 100644 src/stories/TagMultiselectTemplate.tsx diff --git a/src/components/ComboBox.tsx b/src/components/ComboBox.tsx index ce832598..2e555b47 100644 --- a/src/components/ComboBox.tsx +++ b/src/components/ComboBox.tsx @@ -67,6 +67,7 @@ type ComboBoxProps = Exclude & { onDeleteChip?: (key: string) => void inputContent?: ComponentProps['inputContent'] onDeleteInputContent?: ComponentProps['onDeleteInputContent'] + containerProps?: HTMLAttributes } & Pick & Omit< ComboBoxStateOptions, @@ -251,6 +252,7 @@ function ComboBox({ chips, inputContent, onDeleteChip: onDeleteChipProp, + containerProps, ...props }: ComboBoxProps) { const nextFocusedKeyRef = useRef(null) @@ -514,7 +516,7 @@ function ComboBox({ ) return ( - + ) => void + onFilterChange?: (value: string) => void +}) { + const theme = useTheme() + const [selectedTagKeys, setSelectedTagKeys] = useState(new Set()) + const selectedTagArr = useMemo(() => [...selectedTagKeys], [selectedTagKeys]) + const [inputValue, setInputValue] = useState('') + const [isOpen, setIsOpen] = useState(false) + const [searchLogic, setSearchLogic] = useState(matchOptions[0].value) + + const onSelectionChange: ComponentProps< + typeof ComboBox + >['onSelectionChange'] = (key) => { + if (key) { + setSelectedTagKeys(new Set([...selectedTagArr, key])) + setInputValue('') + } + } + + useEffect(() => { + onSelectedTagsChange?.(selectedTagKeys) + }, [selectedTagKeys, onSelectedTagsChange]) + + useEffect(() => { + onFilterChange?.(inputValue) + }, [inputValue, onFilterChange]) + + const onInputChange: ComponentProps['onInputChange'] = ( + value + ) => { + setInputValue(value) + } + + return ( + + + ({ + key, + children: key, + }))} + onDeleteChip={(chipKey) => { + const newKeys = new Set(selectedTagKeys) + + newKeys.delete(chipKey) + setSelectedTagKeys(newKeys) + }} + inputProps={{ + placeholder: 'Search Tags...', + style: { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + backgroundColor: theme.colors['fill-one'], + }, + }} + onOpenChange={(isOpen, _trigger) => { + setIsOpen(isOpen) + }} + maxHeight={232} + allowsEmptyCollection + loading={loading} + containerProps={{ style: { flexGrow: 1 } }} + > + {options + .map((tagStr) => { + if (selectedTagKeys.has(tagStr)) { + return null + } + + return ( + + {tagStr} + + } + textValue={tagStr} + /> + ) + }) + .filter(isNonNullable)} + + + ) +} diff --git a/src/index.ts b/src/index.ts index 137ce760..e1731c2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,6 +103,7 @@ export type { export { default as LoadingSpinner } from './components/LoadingSpinner' export { default as LoopingLogo } from './components/LoopingLogo' export { ComboBox } from './components/ComboBox' +export { TagMultiSelect } from './components/TagMultiSelect' export { Toast, GraphQLToast } from './components/Toast' export { default as WrapWithIf } from './components/WrapWithIf' export type { diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index f7121fae..c288ec02 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -3,6 +3,8 @@ import { type ComponentProps, type Key, useMemo, useState } from 'react' import styled from 'styled-components' import Fuse from 'fuse.js' +import { isEqual, uniqWith } from 'lodash-es' + import { AppIcon, BrowseAppsIcon, @@ -16,6 +18,7 @@ import { } from '..' import { ClusterTagsTemplate } from './ClusterTagsTemplate' +import TagMultiSelectTemplate from './TagMultiselectTemplate' export default { title: 'Combo Box', @@ -486,3 +489,34 @@ ClusterTags.args = { loading: false, withTitleContent: false, } + +const TAGS = [ + { name: 'local', value: 'true' }, + { name: 'local', value: 'false' }, + { name: 'stage', value: 'dev' }, + { name: 'stage', value: 'prod' }, + { name: 'stage', value: 'canary' }, + { name: 'route', value: 'some-very-very-long-tag-value' }, + { name: 'route', value: 'short-name' }, + { name: 'local2', value: 'true' }, + { name: 'local2', value: 'false' }, + { name: 'stage2', value: 'dev' }, + { name: 'stage2', value: 'prod' }, + { name: 'stage2', value: 'canary' }, + { name: 'route2', value: 'some-very-very-long-tag-value' }, + { name: 'route2', value: 'short-name' }, +] +const tags = uniqWith(TAGS, isEqual) + +export const TagMultiSelect = TagMultiSelectTemplate.bind({}) +TagMultiSelect.args = { + loading: false, + options: tags.map((tag) => `${tag.name}:${tag.value}`), + width: 100, + onSelectedTagsChange: (keys: Set) => { + console.log('Selected keys:', keys) + }, + onFilterChange: (filter: string) => { + console.log('Filter:', filter) + }, +} diff --git a/src/stories/TagMultiselectTemplate.tsx b/src/stories/TagMultiselectTemplate.tsx new file mode 100644 index 00000000..2ca7220e --- /dev/null +++ b/src/stories/TagMultiselectTemplate.tsx @@ -0,0 +1,28 @@ +import { type Key } from 'react' + +import { TagMultiSelect } from '../components/TagMultiSelect' + +export default function TagMultiSelectTemplate({ + loading, + options, + width, + onSelectedTagsChange, + onFilterChange, +}: { + loading: boolean + options: string[] + width: number + onSelectedTagsChange?: (keys: Set) => void + onFilterChange?: (value: string) => void +}) { + return ( +
+ +
+ ) +}