Skip to content

Commit

Permalink
feat: create component for tag multi select with new design (#584)
Browse files Browse the repository at this point in the history
  • Loading branch information
renemennab committed Apr 22, 2024
1 parent b78be4c commit 0443b7a
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/components/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type ComboBoxProps = Exclude<ComboBoxInputProps, 'children'> & {
onDeleteChip?: (key: string) => void
inputContent?: ComponentProps<typeof Input2>['inputContent']
onDeleteInputContent?: ComponentProps<typeof Input2>['onDeleteInputContent']
containerProps?: HTMLAttributes<HTMLDivElement>
} & Pick<InputProps, 'suffix' | 'prefix' | 'titleContent' | 'showClearButton'> &
Omit<
ComboBoxStateOptions<object>,
Expand Down Expand Up @@ -251,6 +252,7 @@ function ComboBox({
chips,
inputContent,
onDeleteChip: onDeleteChipProp,
containerProps,
...props
}: ComboBoxProps) {
const nextFocusedKeyRef = useRef<Key>(null)
Expand Down Expand Up @@ -514,7 +516,7 @@ function ComboBox({
)

return (
<ComboBoxInner>
<ComboBoxInner {...containerProps}>
<ComboBoxInput
inputRef={inputInnerRef}
inputProps={{
Expand Down
158 changes: 158 additions & 0 deletions src/components/TagMultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Flex } from 'honorable'
import {
type ComponentProps,
type Key,
useEffect,
useMemo,
useState,
} from 'react'

import { useTheme } from 'styled-components'

import { Chip, ComboBox, ListBoxItem, Select, SelectButton } from '..'

import { isNonNullable } from '../utils/isNonNullable'

export type MultiSelectTag = {
name: string
value: string
}

const matchOptions = [
{ label: 'All', value: 'AND' },
{ label: 'Any', value: 'OR' },
]

export function TagMultiSelect({
options,
loading,
onSelectedTagsChange,
onFilterChange,
}: {
options: string[]
loading: boolean
onSelectedTagsChange?: (keys: Set<Key>) => void
onFilterChange?: (value: string) => void
}) {
const theme = useTheme()
const [selectedTagKeys, setSelectedTagKeys] = useState(new Set<Key>())
const selectedTagArr = useMemo(() => [...selectedTagKeys], [selectedTagKeys])
const [inputValue, setInputValue] = useState('')
const [isOpen, setIsOpen] = useState(false)
const [searchLogic, setSearchLogic] = useState<string>(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<typeof ComboBox>['onInputChange'] = (
value
) => {
setInputValue(value)
}

return (
<Flex
flexDirection="row"
width="100%"
>
<Select
label="Pick search logic"
selectedKey={searchLogic}
onSelectionChange={(key: string) => {
setSearchLogic(key)
}}
defaultOpen={false}
triggerButton={
<SelectButton
css={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderRight: `none`,
width: '150px',
}}
>
Match {matchOptions.find((el) => el.value === searchLogic).label}
</SelectButton>
}
>
{matchOptions.map(({ value, label }) => (
<ListBoxItem
key={value}
label={label}
textValue={label}
/>
))}
</Select>
<ComboBox
isOpen={isOpen}
startIcon={null}
inputValue={inputValue}
onSelectionChange={onSelectionChange}
onInputChange={onInputChange}
chips={selectedTagArr.map((key) => ({
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 (
<ListBoxItem
key={tagStr}
label={
<Chip
size="small"
label={tagStr}
textValue={tagStr}
>
{tagStr}
</Chip>
}
textValue={tagStr}
/>
)
})
.filter(isNonNullable)}
</ComboBox>
</Flex>
)
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions src/stories/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +18,7 @@ import {
} from '..'

import { ClusterTagsTemplate } from './ClusterTagsTemplate'
import TagMultiSelectTemplate from './TagMultiselectTemplate'

export default {
title: 'Combo Box',
Expand Down Expand Up @@ -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<Key>) => {
console.log('Selected keys:', keys)
},
onFilterChange: (filter: string) => {
console.log('Filter:', filter)
},
}
28 changes: 28 additions & 0 deletions src/stories/TagMultiselectTemplate.tsx
Original file line number Diff line number Diff line change
@@ -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<Key>) => void
onFilterChange?: (value: string) => void
}) {
return (
<div style={{ width: `${width}%` }}>
<TagMultiSelect
loading={loading}
options={options}
onSelectedTagsChange={onSelectedTagsChange}
onFilterChange={onFilterChange}
/>
</div>
)
}

0 comments on commit 0443b7a

Please sign in to comment.