-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(select): implement select with search and multiple select (#212)
BREAKING CHANGE: The Boemly select component has been updated. change `onChange` now returns an array of selected strings instead of the event `value` is now a string array instead of a string Migration Steps: update `onChange` handlers to handle the new array structure update `value` to be a string array
- Loading branch information
Showing
12 changed files
with
806 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import React from 'react'; | ||
import { Meta, StoryFn } from '@storybook/react'; | ||
import { BoemlySelect as Select } from './Select'; | ||
|
||
export default { | ||
title: 'Components/Select', | ||
component: Select, | ||
argTypes: { | ||
placeholder: { control: { type: 'text' } }, | ||
isDisabled: { control: { type: 'boolean' } }, | ||
isInvalid: { control: { type: 'boolean' } }, | ||
isFullWidth: { control: { type: 'boolean' } }, | ||
isSearchable: { control: { type: 'boolean' } }, | ||
isMultiple: { control: { type: 'boolean' } }, | ||
onChange: { action: 'Select Changed' }, | ||
options: { control: 'object' }, | ||
color: { | ||
control: { type: 'text' }, | ||
}, | ||
backgroundColor: { | ||
control: { type: 'text' }, | ||
}, | ||
borderColor: { | ||
control: { type: 'text' }, | ||
}, | ||
size: { | ||
options: ['xs', 'sm', 'md', 'lg'], | ||
defaultValue: 'md', | ||
control: { type: 'radio' }, | ||
}, | ||
variant: { | ||
options: ['filled', 'unstyled', 'flushed', 'outline'], | ||
defaultValue: 'filled', | ||
control: { type: 'radio' }, | ||
}, | ||
}, | ||
} as Meta<typeof Select>; | ||
|
||
const Template: StoryFn<typeof Select> = (args) => <Select {...args} />; | ||
|
||
export const Default = Template.bind({}); | ||
Default.args = { | ||
placeholder: 'Select an option', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
], | ||
}; | ||
|
||
export const WithSelectedValue = Template.bind({}); | ||
WithSelectedValue.args = { | ||
value: ['option_2'], | ||
placeholder: 'Select an option', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
], | ||
}; | ||
|
||
export const WithPlaceholder = Template.bind({}); | ||
WithPlaceholder.args = { | ||
placeholder: 'Placeholder', | ||
color: 'black', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
], | ||
}; | ||
|
||
export const Searchable = Template.bind({}); | ||
Searchable.args = { | ||
isSearchable: true, | ||
placeholder: 'Search options...', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
], | ||
}; | ||
|
||
export const MultiSelect = Template.bind({}); | ||
MultiSelect.args = { | ||
isMultiple: true, | ||
placeholder: 'Select multiple options', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
], | ||
}; | ||
|
||
export const SearchableMultiSelect = Template.bind({}); | ||
SearchableMultiSelect.args = { | ||
isSearchable: true, | ||
isMultiple: true, | ||
placeholder: 'Search and select multiple options', | ||
searchPlaceholder: 'Search for a content...', | ||
options: [ | ||
{ label: 'Option 1', value: 'option_1' }, | ||
{ label: 'Option 2', value: 'option_2' }, | ||
{ label: 'Option 3', value: 'option_3' }, | ||
{ label: 'Option 4', value: 'option_4' }, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import React from 'react'; | ||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
import { Select } from '.'; | ||
|
||
const mockOptions = [ | ||
{ label: 'Option 1', value: '1' }, | ||
{ label: 'Option 2', value: '2' }, | ||
{ label: 'Option 3', value: '3' }, | ||
]; | ||
|
||
describe('The Select component', () => { | ||
it('renders with placeholder text', () => { | ||
render( | ||
<Select | ||
options={mockOptions} | ||
color="white" | ||
placeholder="Select an option from these options" | ||
/> | ||
); | ||
expect(screen.getByText('Select an option from these options')).toBeInTheDocument(); | ||
}); | ||
|
||
it('opens dropdown when clicked', () => { | ||
render(<Select color="black" options={mockOptions} />); | ||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
expect(screen.getByText('Option 1')).toBeInTheDocument(); | ||
}); | ||
|
||
it('selects an option when clicked', () => { | ||
render(<Select color="black" options={mockOptions} />); | ||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
fireEvent.click(screen.getByText('Option 1')); | ||
expect(screen.getByText('Option 1')).toBeInTheDocument(); | ||
}); | ||
|
||
it('handles single selection mode', () => { | ||
render(<Select color="black" options={mockOptions} value={['1']} />); | ||
expect(screen.getByText('Option 1')).toBeInTheDocument(); | ||
}); | ||
|
||
it('handles multiple selection mode', () => { | ||
render( | ||
<Select | ||
color="white" | ||
options={mockOptions} | ||
isMultiple | ||
selectAllText="Select All" | ||
clearAllText="Clear All" | ||
/> | ||
); | ||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
fireEvent.click(screen.getByText('Option 1')); | ||
fireEvent.click(screen.getByText('Option 2')); | ||
expect(screen.getByText('Option 1')).toBeInTheDocument(); | ||
expect(screen.getByText('Option 2')).toBeInTheDocument(); | ||
}); | ||
|
||
it('filters options when search term is entered', () => { | ||
render( | ||
<Select | ||
color="black" | ||
options={mockOptions} | ||
isSearchable | ||
selectAllText="Select All" | ||
clearAllText="Clear All" | ||
/> | ||
); | ||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
fireEvent.change(screen.getByPlaceholderText('Select an option'), { | ||
target: { value: 'Option 1' }, | ||
}); | ||
expect(screen.getByText('Option 1')).toBeInTheDocument(); | ||
expect(screen.queryByText('Option 2')).toBeNull(); | ||
}); | ||
|
||
it('selects all options in multiple mode', () => { | ||
render( | ||
<Select | ||
color="black" | ||
options={mockOptions} | ||
isMultiple | ||
selectAllText="Select All" | ||
clearAllText="Clear All" | ||
/> | ||
); | ||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
fireEvent.click(screen.getByText('Select All')); | ||
|
||
// Find the options and navigate to their checkboxes | ||
const option1Text = screen.queryByText('Option 1'); | ||
const option2Text = screen.queryByText('Option 2'); | ||
const option3Text = screen.queryByText('Option 3'); | ||
|
||
// Get the closest parent div for each option | ||
const option1Div = option1Text?.closest('div'); | ||
const option2Div = option2Text?.closest('div'); | ||
const option3Div = option3Text?.closest('div'); | ||
|
||
// Check if the checkbox within the div have a data-checked attribute | ||
expect(option1Div?.querySelector('label')).toHaveAttribute('data-checked'); | ||
expect(option2Div?.querySelector('label')).toHaveAttribute('data-checked'); | ||
expect(option3Div?.querySelector('label')).toHaveAttribute('data-checked'); | ||
}); | ||
|
||
it('clears all selected options in multiple mode', () => { | ||
render( | ||
<Select | ||
color="black" | ||
options={mockOptions} | ||
isMultiple | ||
selectAllText="Select All" | ||
clearAllText="Clear All" | ||
/> | ||
); | ||
|
||
fireEvent.click(screen.getByRole('button', { name: /toggle dropdown/i })); | ||
|
||
fireEvent.click(screen.getByText('Select All')); | ||
|
||
fireEvent.click(screen.getByText('Clear All')); | ||
|
||
// Find the options and navigate to their checkboxes | ||
const option1Text = screen.queryByText('Option 1'); | ||
const option2Text = screen.queryByText('Option 2'); | ||
const option3Text = screen.queryByText('Option 3'); | ||
|
||
// Get the closest parent div for each option | ||
const option1Div = option1Text?.closest('div'); | ||
const option2Div = option2Text?.closest('div'); | ||
const option3Div = option3Text?.closest('div'); | ||
|
||
// Check if the checkbox within the div does not have a data-checked attribute | ||
expect(option1Div?.querySelector('label')).not.toHaveAttribute('data-checked'); | ||
expect(option2Div?.querySelector('label')).not.toHaveAttribute('data-checked'); | ||
expect(option3Div?.querySelector('label')).not.toHaveAttribute('data-checked'); | ||
}); | ||
|
||
it('when dropdown is opened and closed, aria-expanded value changes', () => { | ||
render(<Select color="black" options={mockOptions} />); | ||
|
||
const toggleButton = screen.getByRole('combobox'); | ||
|
||
expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); | ||
|
||
fireEvent.click(toggleButton); | ||
|
||
expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); | ||
|
||
fireEvent.click(toggleButton); | ||
|
||
expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); | ||
}); | ||
}); |
Oops, something went wrong.