Skip to content

Commit

Permalink
test(combobox): tests for combobox blur behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
Powerplex committed Feb 20, 2024
1 parent ae12a2d commit 41d4dc8
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 35 deletions.
2 changes: 1 addition & 1 deletion packages/components/combobox/src/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default meta
export const Default: StoryFn = _args => {
return (
<div className="pb-[300px]">
<Combobox>
<Combobox autoFilter={false}>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
<Combobox.Disclosure openedLabel="Close popup" closedLabel="Open popup" />
Expand Down
21 changes: 2 additions & 19 deletions packages/components/combobox/src/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,8 @@ import { type ComboboxContextProps, ComboboxProvider } from './ComboboxContext'

export type ComboboxProps = ComboboxContextProps

export const Combobox = ({
children,
autoFilter = true,
disabled = false,
readOnly = false,
allowCustomValue = false,
...props
}: ComboboxProps) => {
return (
<ComboboxProvider
autoFilter={autoFilter}
disabled={disabled}
readOnly={readOnly}
allowCustomValue={allowCustomValue}
{...props}
>
{children}
</ComboboxProvider>
)
export const Combobox = ({ children, ...props }: ComboboxProps) => {
return <ComboboxProvider {...props}>{children}</ComboboxProvider>
}

Combobox.displayName = 'Combobox'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'

Expand Down Expand Up @@ -114,5 +114,73 @@ describe('Combobox', () => {
expect(getInput('Book').getAttribute('placeholder')).toBe('Pick a book')
expect(getItem('1984')).toHaveAttribute('aria-selected', 'false')
})

describe('blur behaviour', () => {
it('should not clear input value when custom value is allowed', async () => {
const user = userEvent.setup()

// Given a combobox that allows custom input value
render(
<Combobox multiple allowCustomValue>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then placeholder should be displayed
expect(getInput('Book').getAttribute('placeholder')).toBe('Pick a book')

// When the user type "pri" in the input
await user.click(getInput('Book'))
await user.keyboard('{p}{r}{i}')

// When the user focus leaves the input
await user.click(document.body)

// Then input value is preserved as custom values are allowed
expect(screen.getByDisplayValue('pri')).toBeInTheDocument()
})

it('should clear input value if custom value is not allowed', async () => {
const user = userEvent.setup()

// Given a combobox that does not allow custom input value
render(
<Combobox multiple allowCustomValue={false}>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then placeholder should be displayed
expect(getInput('Book').getAttribute('placeholder')).toBe('Pick a book')

// When the user type "pri" to filter first item matching, then select it
await user.click(getInput('Book'))
await user.keyboard('{p}{r}{i}')

// When the user focus leaves the input
await user.click(document.body)

// Then input value has been cleared
expect(screen.getByDisplayValue('')).toBeInTheDocument()
})
})
})
})
169 changes: 169 additions & 0 deletions packages/components/combobox/src/tests/singleSelection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,174 @@ describe('Combobox', () => {
expect(queryItem('1984')).not.toBeInTheDocument()
expect(getItem('Pride and Prejudice')).toHaveAttribute('aria-selected', 'true')
})

describe('blur behaviour', () => {
it('should not clear input value when custom value is allowed', async () => {
const user = userEvent.setup()

// Given a combobox that allows custom input value
render(
<Combobox allowCustomValue>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then placeholder should be displayed
expect(getInput('Book').getAttribute('placeholder')).toBe('Pick a book')

// When the user type "pri" in the input
await user.click(getInput('Book'))
await user.keyboard('{p}{r}{i}')

// When the user focus leaves the input
await user.click(document.body)

// Then input value is preserved as custom values are allowed
expect(screen.getByDisplayValue('pri')).toBeInTheDocument()
})

it('should clear input value if custom value is not allowed', async () => {
const user = userEvent.setup()

// Given a combobox that does not allow custom input value
render(
<Combobox>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then placeholder should be displayed
expect(getInput('Book').getAttribute('placeholder')).toBe('Pick a book')

// When the user type "pri" to filter first item matching, then select it
await user.click(getInput('Book'))
await user.keyboard('{p}{r}{i}')

// When the user focus leaves the input
await user.click(document.body)

// Then input value has been cleared
expect(screen.getByDisplayValue('')).toBeInTheDocument()
})

it('should clear selected item if input value is an empty string', async () => {
const user = userEvent.setup()

// Given a combobox that does not allow custom input value
render(
<Combobox defaultValue="book-2">
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then 1984 should be selected
expect(getItem('1984')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByDisplayValue('1984')).toBeInTheDocument()

// When the user clears the input and focus outside of it
await user.clear(screen.getByDisplayValue('1984'))
await user.click(document.body)

// Then item has been unselected and input remains cleared
expect(getItem('1984')).toHaveAttribute('aria-selected', 'false')
expect(screen.getByDisplayValue('')).toBeInTheDocument()
})

it('should update input value to matching item if value matches precisely', async () => {
const user = userEvent.setup()

// Given a combobox that does not allow custom input value
render(
<Combobox defaultValue="book-2" autoFilter={false}>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then 1984 should be selected
expect(getItem('1984')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByDisplayValue('1984')).toBeInTheDocument()

const input = getInput('Book')
// When the user changes the input to the value of another item and focus outside of it
await user.clear(input)
await user.type(input, 'war and peace') // notice this is not case-sensitive
await user.click(document.body)

// Then item has been unselected and item matching the input value has been selected
expect(getItem('1984')).toHaveAttribute('aria-selected', 'false')
expect(getItem('War and Peace')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByDisplayValue('War and Peace')).toBeInTheDocument()
})

it('should sync input value to selected value if value does not match', async () => {
const user = userEvent.setup()

// Given a combobox that does not allow custom input value
render(
<Combobox defaultValue="book-2" autoFilter={false}>
<Combobox.Trigger>
<Combobox.Input aria-label="Book" placeholder="Pick a book" />
</Combobox.Trigger>
<Combobox.Popover>
<Combobox.Items>
<Combobox.Item value="book-1">War and Peace</Combobox.Item>
<Combobox.Item value="book-2">1984</Combobox.Item>
<Combobox.Item value="book-3">Pride and Prejudice</Combobox.Item>
</Combobox.Items>
</Combobox.Popover>
</Combobox>
)

// Then 1984 should be selected
expect(getItem('1984')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByDisplayValue('1984')).toBeInTheDocument()

const input = getInput('Book')
// When the user changes the input to the value of another item and focus outside of it
await user.clear(input)
await user.type(input, 'A value that does not match any item')
await user.click(document.body)

// Then item remain selected and the input is synced with its text value
expect(getItem('1984')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByDisplayValue('1984')).toBeInTheDocument()
})
})
})
})
29 changes: 15 additions & 14 deletions packages/components/combobox/src/useCombobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export const useCombobox = ({
state,
{ changes, type }
) => {
const match = [...itemsMap.values()].find(item => item.text === state.inputValue)
const match = [...itemsMap.values()].find(
item => item.text.toLowerCase() === state.inputValue.toLowerCase()
)

const fallbackText = state.selectedItem?.text || ''

switch (type) {
case useDownshiftCombobox.stateChangeTypes.InputBlur:
Expand All @@ -97,15 +101,15 @@ export const useCombobox = ({
}

if (match) {
return { ...changes, selectedItem: match }
} else {
const newinputValue = state.selectedItem?.text || ''
updateInputValue(newinputValue)
updateInputValue(match.text)

return { ...changes, inputValue: newinputValue }
return { ...changes, selectedItem: match, inputValue: match.text }
}

return changes
updateInputValue(fallbackText)

return { ...changes, inputValue: fallbackText }

default:
return changes
}
Expand All @@ -125,14 +129,11 @@ export const useCombobox = ({

switch (type) {
case useDownshiftCombobox.stateChangeTypes.InputBlur:
if (allowCustomValue) {
return changes
} else {
const newinputValue = ''
updateInputValue(newinputValue)
if (allowCustomValue) return changes

return { ...changes, inputValue: newinputValue }
}
updateInputValue('')

return { ...changes, inputValue: '' }

case useDownshiftCombobox.stateChangeTypes.InputKeyDownEnter:
case useDownshiftCombobox.stateChangeTypes.ItemClick:
Expand Down

0 comments on commit 41d4dc8

Please sign in to comment.