Skip to content

Commit

Permalink
Merge pull request #853 from laboratoriobridge/823-combobox-treating-…
Browse files Browse the repository at this point in the history
…select-issues

#823 [Combobox] - Treating select issues
  • Loading branch information
Andrevmatias authored Sep 24, 2024
2 parents 114cd14 + 0152abb commit 52b576d
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 22 deletions.
Binary file modified .loki/reference/chrome_laptop_Components_Combobox_Multi_Select.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/components/Combobox/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export const MultiSelect = () => {
clearable={boolean('clearable', true)}
disabled={boolean('disabled', false)}
openOnFocus={boolean('openOnFocus', true)}
clearFilterOnSelect={boolean('clearFilterOnSelect', true)}
open={select('open', [undefined, true, false], undefined)}
loading={boolean('loading', false)}
onChange={action('changed')}
Expand Down
49 changes: 40 additions & 9 deletions src/components/Combobox/ComboboxMultiselect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,31 @@ describe('async loading', () => {
})

describe('filtering', () => {
it('should keep the current filter after an item is selected', async () => {
const { container } = render(<ComboboxTest />)
test.each`
clearFilterOnSelect
${true}
${false}
`(
'should clear the current filter and the input value after menu is closed (clearFilterOnSelect: $clearFilterOnSelect)',
async ({ clearFilterOnSelect }) => {
const { container } = render(<ComboboxTest clearFilterOnSelect={clearFilterOnSelect} />)
const input = container.querySelector('input')!

fireEvent.focus(input)
fireEvent.change(input, { target: { value: 'pe' } })

await waitFor(() => expect(container.querySelectorAll('li')).toHaveLength(4))

fireEvent.click(container.querySelectorAll('li')[0])
fireEvent.blur(input)

expect(container.querySelectorAll('li')).toHaveLength(0)
expect(input.value).toEqual('')
}
)

it('should clear the current filter and the input value after an item is selected if clearFilterOnSelect is true', async () => {
const { container } = render(<ComboboxTest clearFilterOnSelect={true} />)
const input = container.querySelector('input')!

fireEvent.focus(input)
Expand All @@ -555,20 +578,28 @@ describe('filtering', () => {

fireEvent.click(container.querySelectorAll('li')[0])

expect(input.value).toEqual('pe')
expect(container.querySelectorAll('li')).toHaveLength(4)
await waitFor(() => expect(input.value).toEqual(''))
await waitFor(() => expect(container.querySelectorAll('li')).toHaveLength(fruits.length))
})
it('should clear the current filter and the input value after menu is closed', async () => {
const { container } = render(<ComboboxTest />)

it('should not clear the current filter and the input value after an item is selected if clearFilterOnSelect is false', async () => {
const { container } = render(<ComboboxTest clearFilterOnSelect={false} />)
const input = container.querySelector('input')!

fireEvent.focus(input)
fireEvent.change(input, { target: { value: 'pe' } })

await waitFor(() => expect(container.querySelectorAll('li')).toHaveLength(4))

fireEvent.click(container.querySelectorAll('li')[0])
fireEvent.blur(input)

expect(container.querySelectorAll('li')).toHaveLength(0)
expect(input.value).toEqual('')
expect(container.querySelectorAll('li')).toHaveLength(4)
expect(input.value).toEqual('pe')

fireEvent.click(container.querySelectorAll('li')[1])

expect(container.querySelectorAll('li')).toHaveLength(4)
expect(input.value).toEqual('pe')
})
})

Expand Down
46 changes: 35 additions & 11 deletions src/components/Combobox/ComboboxMultiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import matchSorter from 'match-sorter'
import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { usePopper } from 'react-popper'
import { isNil } from 'lodash'
import { useLocale } from '../../i18n'
import { Theme, useStyles } from '../../styles'
import { composeHandlers, composeRefs } from '../../util/react'
import { FormControl } from '../FormControl'
Expand All @@ -22,6 +23,7 @@ export interface ComboboxMultiselectProps<T>
onChange?: (newValue: T[]) => void
itemIsEqual(a: T, b: T): boolean
components?: Partial<ComboboxMultiselectComponents<T>>
clearFilterOnSelect?: boolean
}

export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: ComboboxMultiselectProps<T>) {
Expand All @@ -41,6 +43,7 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
itemToString,
menuMinWidth,
openOnFocus = true,
clearFilterOnSelect = false,
onClear,
onChange,
onFocus,
Expand All @@ -58,6 +61,7 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
} = props

const [itemsLoaded, setItemsLoaded] = useState(false)
const locale = useLocale()

const isAsync = typeof items === 'function'
const getItems = useCallback((query: string) => (typeof items === 'function' ? items(query) : filter(items, query)), [
Expand Down Expand Up @@ -120,6 +124,7 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
getItemProps,
openMenu,
setInputValue,
toggleMenu,
} = useCombobox<T>({
defaultHighlightedIndex: 0,
selectedItem: null,
Expand All @@ -130,7 +135,7 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
onInputValueChange: ({ inputValue }) => composeHandlers(loadItems, onFilterChange)(inputValue),
onSelectedItemChange: ({ selectedItem }) => {
isSelected(selectedItem) ? removeSelectedItem(selectedItem) : addSelectedItem(selectedItem)
setInputValue('')
clearFilterOnSelect && setInputValue('')
},
onIsOpenChange: ({ isOpen, inputValue }) => {
isOpen && !itemsLoaded && loadItems(inputValue)
Expand Down Expand Up @@ -170,7 +175,10 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox

const handleWrapperClick = () => inputRef.current.focus()
const handleItemClick = useCallback(
(item: T) => (isSelected(item) ? removeSelectedItem(item) : addSelectedItem(item)),
(item: T) => {
isSelected(item) ? removeSelectedItem(item) : addSelectedItem(item)
clearFilterOnSelect && setInputValue('')
},
[isSelected, removeSelectedItem, addSelectedItem]

Check warning on line 182 in src/components/Combobox/ComboboxMultiselect.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useCallback has missing dependencies: 'clearFilterOnSelect' and 'setInputValue'. Either include them or remove the dependency array
)
const handleItemRemove = useCallback(
Expand All @@ -190,6 +198,11 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
[components]
)

const handleIconClick = useCallback(() => {
toggleMenu()
inputRef.current?.focus()
}, [toggleMenu])

return (
<div {...downshiftComboboxProps}>
<FormControl {...formControlProps} labelId={internalLabelId} {...downshiftLabelProps}>
Expand All @@ -199,10 +212,13 @@ export function ComboboxMultiselect<T = DefaultComboboxItemType>(props: Combobox
onClick={handleWrapperClick}
clearVisible={clearable && !!selectedItems.length}
onClear={composeHandlers(reset, onClear)}
icon={isOpen ? 'angleUp' : 'angleDown'}
iconAriaLabel={isOpen ? locale.combobox.hideOptions : locale.combobox.showOptions}
iconPosition='right'
onIconClick={handleIconClick}
>
{selectedItems.map((selectedItem, index) => (
<SelectedItem
style={classes.selectedItem}
key={`selected-item-${index}`}
onRemove={handleItemRemove(selectedItem)}
disabled={disabled}
Expand Down Expand Up @@ -264,30 +280,35 @@ function comboboxMultiselectStateReducer<T>(
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
...(!changes.selectedItem && {
inputValue: '',
}),
inputValue: '',
}
default:
return changes
}
}

export const createStyles = (theme: Theme, { disabled }: ComboboxMultiselectProps<any>, hasSelectedItems: boolean) => {
export const createStyles = (
theme: Theme,
{ disabled, clearable }: ComboboxMultiselectProps<any>,
hasSelectedItems: boolean
) => {
const parts = createStyleParts(theme)
return {
wrapper: {
...parts.base,
cursor: 'text',

display: 'flex',
gap: '0.25rem',
flexWrap: 'wrap',
alignItems: 'center',

padding: hasSelectedItems ? 'calc(0.25rem - 1px) 0.25rem' : 'calc(0.5rem - 1px) 0.5rem',
'&:hover': !disabled && parts.hover,
'&:active': !disabled && parts.active,
'&:focus-within': !disabled && parts.focus,

paddingRight: clearable && hasSelectedItems ? '5rem' : '3rem',
} as CSSProperties,

disabled: parts.disabled,
Expand All @@ -297,22 +318,25 @@ export const createStyles = (theme: Theme, { disabled }: ComboboxMultiselectProp
'&:focus-within': parts.invalid[':not(:disabled):focus'],
} as CSSProperties,

selectedItem: {
marginRight: '0.25rem',
} as CSSProperties,

input: {
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.sizes.text,
color: theme.pallete.text.main,
lineHeight: '1rem',
background: theme.pallete.surface.main,
padding: 0,
paddingRight: '0 !important',
minWidth: '5rem',
flex: 1,
border: 0,
outline: 0,
'::placeholder': parts.placeholder,
':disabled': parts.disabled,
'~ span': {
top: 0,
right: 0,
bottom: 0,
},
} as CSSProperties,

menu: {
Expand Down
5 changes: 3 additions & 2 deletions src/components/Combobox/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ function ListBoxInner<T>(props: ListBoxProps<T>, ref: Ref<HTMLDivElement>) {
createNewItem,
isItemSelected,
onItemClick,
tabIndex,
...rest
} = props
const { classes } = useStyles(createStyles)

const { CreateItem, AppendItem, EmptyItem, Item, LoadingItem, PrependItem } = components

return (
<div ref={ref} {...rest}>
<ul className={classes.list}>
<div ref={ref} tabIndex={tabIndex} {...rest}>
<ul className={classes.list} tabIndex={tabIndex}>
{PrependItem && <PrependItem />}
{loading && <LoadingItem />}
{!loading && createNewItem && !items?.length && <CreateItem />}
Expand Down

0 comments on commit 52b576d

Please sign in to comment.