Skip to content

Commit

Permalink
feat: add new options to TagMultiSelect (#625)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsladerman authored Jul 18, 2024
1 parent cd4d732 commit 3c164fe
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 119 deletions.
3 changes: 3 additions & 0 deletions src/components/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type ComboBoxProps = Exclude<ComboBoxInputProps, 'children'> & {
onHeaderClick?: () => unknown
onFooterClick?: () => unknown
startIcon?: ReactNode
endIcon?: ReactNode
placement?: Placement
width?: string | number
maxHeight?: string | number
Expand Down Expand Up @@ -241,6 +242,7 @@ function ComboBox({
onFooterClick,
showArrow,
startIcon,
endIcon,
placement,
width,
maxHeight,
Expand Down Expand Up @@ -537,6 +539,7 @@ function ComboBox({
showClearButton={showClearButton}
setIsOpen={setIsOpen}
startIcon={startIcon}
endIcon={endIcon}
outerInputProps={outerInputProps}
loading={loading}
hasChips={!!chips}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Flyover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const FlyoverHeaderWrapSC = styled.div(({ theme }) => ({
gap: theme.spacing.small,
height: 56,
borderBottom: `1px solid ${theme.colors.border}`,
backgroundColor: theme.colors.grey[950],
backgroundColor: theme.colors['fill-zero'],
display: 'flex',
padding: `${theme.spacing.small}px ${theme.spacing.medium}px`,
}))
Expand Down
1 change: 0 additions & 1 deletion src/components/ListBoxItemChipList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ const ChipListUnstyled = forwardRef<HTMLDivElement, ChipListProps>(

const ChipList = styled(ChipListUnstyled)(({ theme }) => ({
display: 'flex',
flexDiretion: 'row',
flexWrap: 'wrap',
justifyContent: 'end',
gap: theme.spacing.xxsmall,
Expand Down
262 changes: 145 additions & 117 deletions src/components/TagMultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Flex } from 'honorable'
import {
type ComponentProps,
type Key,
useEffect,
useMemo,
useState,
} from 'react'
import { type ComponentProps, type Key, useMemo, useState } from 'react'

import styled, { useTheme } from 'styled-components'
import styled, {
type DefaultTheme,
type StyledComponent,
useTheme,
} from 'styled-components'

import { Chip, ComboBox, ListBoxItem, Select, SelectButton } from '..'
import {
Chip,
ChipList,
ComboBox,
Flex,
ListBoxItem,
Select,
SelectButton,
type SelectPropsSingle,
} from '..'

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

Expand All @@ -26,24 +32,31 @@ const matchOptions = [
type TagMultiSelectProps = {
options: string[]
loading: boolean
innerChips?: boolean
selectedMatchType?: 'AND' | 'OR'
onSelectedTagsChange?: (keys: Set<Key>) => void
onFilterChange?: (value: string) => void
onChangeMatchType?: (value: 'AND' | 'OR') => void
comboBoxProps?: Omit<ComponentProps<typeof ComboBox>, 'children'>
selectProps?: Omit<SelectPropsSingle, 'children'>
}

function TagMultiSelect({
options,
loading,
innerChips = true,
selectedMatchType,
onSelectedTagsChange,
onFilterChange,
selectedTagKeys,
setSelectedTagKeys,
inputValue,
setInputValue,
onChangeMatchType,
}: TagMultiSelectProps) {
comboBoxProps,
selectProps,
...props
}: TagMultiSelectProps & ComponentProps<StyledComponent<'div', DefaultTheme>>) {
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>(
selectedMatchType || matchOptions[0].value
Expand All @@ -58,118 +71,133 @@ function TagMultiSelect({
}
}

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={(value: 'AND' | 'OR') => {
setSearchLogic(value)
onChangeMatchType?.(value)
}}
defaultOpen={false}
triggerButton={
<MultiSelectMatchButtonContainer>
<SelectButton>
Match {matchOptions.find((el) => el.value === searchLogic).label}
</SelectButton>
</MultiSelectMatchButtonContainer>
}
>
{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 } }}
showArrow={false}
>
{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>
<TagMultiSelectWrapperSC {...props}>
<Flex>
<Select
label="Pick search logic"
selectedKey={searchLogic}
onSelectionChange={(value: 'AND' | 'OR') => {
setSearchLogic(value)
onChangeMatchType?.(value)
}}
defaultOpen={false}
triggerButton={
<MultiSelectMatchButtonContainer>
<SelectButton>
{matchOptions.find((el) => el.value === searchLogic).label}
</SelectButton>
</MultiSelectMatchButtonContainer>
}
{...selectProps}
>
{matchOptions.map(({ value, label }) => (
<ListBoxItem
key={value}
label={label}
textValue={label}
/>
))}
</Select>
<ComboBox
isOpen={isOpen}
startIcon={null}
inputValue={inputValue}
onSelectionChange={onSelectionChange}
onInputChange={(value) => setInputValue(value)}
chips={
innerChips
? selectedTagArr.map((key) => ({
key,
children: key,
}))
: undefined
}
onDeleteChip={(chipKey) => {
const newKeys = new Set(selectedTagKeys)

newKeys.delete(chipKey)
setSelectedTagKeys(newKeys)
}}
onOpenChange={(isOpen, _trigger) => {
setIsOpen(isOpen)
}}
maxHeight={232}
allowsEmptyCollection
loading={loading}
containerProps={{ style: { flexGrow: 1 } }}
showArrow={false}
{...comboBoxProps}
inputProps={{
placeholder: 'Search Tags...',
...comboBoxProps?.inputProps,
style: {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
backgroundColor: theme.colors['fill-one'],
...comboBoxProps?.inputProps?.style,
},
}}
>
{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>
{!(innerChips || selectedTagArr.length === 0) && (
<ChipList
size="small"
limit={8}
values={selectedTagArr}
closeButton
onClickCondition={() => true}
onClick={(key: Key) => {
const newKeys = new Set(selectedTagArr)

newKeys.delete(key)
setSelectedTagKeys(newKeys)
}}
/>
)}
</TagMultiSelectWrapperSC>
)
}

export type { TagMultiSelectProps }
export { TagMultiSelect }
export type { TagMultiSelectProps }

const TagMultiSelectWrapperSC = styled.div(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.xsmall,
}))

const MultiSelectMatchButtonContainer = styled.div`
> div {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
width: 150px;
minwidth: fit-content;
text-wrap: nowrap;
}
`

0 comments on commit 3c164fe

Please sign in to comment.