From 41d4dc862a97ae5778f1e8bad85c63dc3d6a39ee Mon Sep 17 00:00:00 2001 From: Powerplex Date: Tue, 20 Feb 2024 15:15:06 +0100 Subject: [PATCH] test(combobox): tests for combobox blur behaviour --- .../combobox/src/Combobox.stories.tsx | 2 +- packages/components/combobox/src/Combobox.tsx | 21 +-- .../src/tests/multipleSelection.test.tsx | 70 +++++++- .../src/tests/singleSelection.test.tsx | 169 ++++++++++++++++++ .../components/combobox/src/useCombobox.ts | 29 +-- 5 files changed, 256 insertions(+), 35 deletions(-) diff --git a/packages/components/combobox/src/Combobox.stories.tsx b/packages/components/combobox/src/Combobox.stories.tsx index 1015bd0dd..b7ea1051b 100644 --- a/packages/components/combobox/src/Combobox.stories.tsx +++ b/packages/components/combobox/src/Combobox.stories.tsx @@ -63,7 +63,7 @@ export default meta export const Default: StoryFn = _args => { return (
- + diff --git a/packages/components/combobox/src/Combobox.tsx b/packages/components/combobox/src/Combobox.tsx index e65cba058..839d79b78 100644 --- a/packages/components/combobox/src/Combobox.tsx +++ b/packages/components/combobox/src/Combobox.tsx @@ -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 ( - - {children} - - ) +export const Combobox = ({ children, ...props }: ComboboxProps) => { + return {children} } Combobox.displayName = 'Combobox' diff --git a/packages/components/combobox/src/tests/multipleSelection.test.tsx b/packages/components/combobox/src/tests/multipleSelection.test.tsx index 581ed0571..81cf6ab7c 100644 --- a/packages/components/combobox/src/tests/multipleSelection.test.tsx +++ b/packages/components/combobox/src/tests/multipleSelection.test.tsx @@ -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' @@ -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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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() + }) + }) }) }) diff --git a/packages/components/combobox/src/tests/singleSelection.test.tsx b/packages/components/combobox/src/tests/singleSelection.test.tsx index c9abbfe5f..7704b2e07 100644 --- a/packages/components/combobox/src/tests/singleSelection.test.tsx +++ b/packages/components/combobox/src/tests/singleSelection.test.tsx @@ -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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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( + + + + + + + War and Peace + 1984 + Pride and Prejudice + + + + ) + + // 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() + }) + }) }) }) diff --git a/packages/components/combobox/src/useCombobox.ts b/packages/components/combobox/src/useCombobox.ts index 7d53528c1..f2e725792 100644 --- a/packages/components/combobox/src/useCombobox.ts +++ b/packages/components/combobox/src/useCombobox.ts @@ -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: @@ -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 } @@ -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: