Skip to content

Commit

Permalink
fix: avoid selecting all options instead of filteres options
Browse files Browse the repository at this point in the history
  • Loading branch information
German Saracca authored and German Saracca committed May 3, 2024
1 parent 8238155 commit 5a12f28
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
selectMultipleReducer,
selectOption,
removeOption,
toggleAllOptions,
selectAllOptions,
deselectAllOptions,
searchOptions
} from './selectMultipleReducer'
import { SelectMultipleToggle } from './SelectMultipleToggle'
Expand Down Expand Up @@ -38,12 +39,14 @@ export const SelectMultiple = forwardRef(
}: SelectMultipleProps,
ref: ForwardedRef<HTMLInputElement | null>
) => {
const [{ selectedOptions, filteredOptions }, dispatch] = useReducer(selectMultipleReducer, {
...selectMultipleInitialState,
options: options,
filteredOptions: options,
selectedOptions: defaultValue || []
})
const [{ selectedOptions, filteredOptions, searchValue }, dispatch] = useReducer(
selectMultipleReducer,
{
...selectMultipleInitialState,
options: options,
selectedOptions: defaultValue || []
}
)
const isFirstRender = useIsFirstRender()
const menuId = useId()

Expand All @@ -70,7 +73,13 @@ export const SelectMultiple = forwardRef(

const handleRemoveSelectedOption = (option: string): void => dispatch(removeOption(option))

const handleToggleAllOptions = (): void => dispatch(toggleAllOptions())
const handleToggleAllOptions = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.checked) {
dispatch(selectAllOptions())
} else {
dispatch(deselectAllOptions())
}
}

return (
<DropdownBS autoClose="outside">
Expand All @@ -87,6 +96,7 @@ export const SelectMultiple = forwardRef(
options={options}
selectedOptions={selectedOptions}
filteredOptions={filteredOptions}
searchValue={searchValue}
handleToggleAllOptions={handleToggleAllOptions}
handleSearch={handleSearch}
handleCheck={handleCheck}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ interface SelectMultipleMenuProps {
options: string[]
selectedOptions: string[]
filteredOptions: string[]
handleToggleAllOptions: () => void
searchValue: string
handleToggleAllOptions: (e: React.ChangeEvent<HTMLInputElement>) => void
handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void
handleCheck: (e: React.ChangeEvent<HTMLInputElement>) => void
isSearchable: boolean
Expand All @@ -17,6 +18,7 @@ export const SelectMultipleMenu = ({
options,
selectedOptions,
filteredOptions,
searchValue,
handleToggleAllOptions,
handleSearch,
handleCheck,
Expand All @@ -26,8 +28,15 @@ export const SelectMultipleMenu = ({
const searchInputControlID = useId()
const toggleAllControlID = useId()

const noOptionsFound = filteredOptions.length === 0
const allOptionsSelected = selectedOptions.length === options.length
const menuOptions = filteredOptions.length > 0 ? filteredOptions : options

const noOptionsFound = searchValue !== '' && filteredOptions.length === 0

const allOptionsShownAreSelected = !noOptionsFound
? filteredOptions.length > 0
? filteredOptions.every((option) => selectedOptions.includes(option))
: options.every((option) => selectedOptions.includes(option))
: false

return (
<DropdownBS.Menu
Expand All @@ -50,7 +59,8 @@ export const SelectMultipleMenu = ({
aria-label="Toggle all options"
id={toggleAllControlID}
onChange={handleToggleAllOptions}
checked={allOptionsSelected}
checked={allOptionsShownAreSelected}
disabled={noOptionsFound}
/>
{isSearchable ? (
<FormBS.Control
Expand All @@ -66,19 +76,20 @@ export const SelectMultipleMenu = ({
)}
</DropdownBS.Header>

{filteredOptions.map((option) => (
<DropdownBS.Item as="li" className={styles['option-item']} key={option}>
<FormBS.Check
type="checkbox"
value={option}
label={option}
onChange={handleCheck}
id={`check-item-${option}`}
checked={selectedOptions.includes(option)}
className={styles['option-item__checkbox-input']}
/>
</DropdownBS.Item>
))}
{!noOptionsFound &&
menuOptions.map((option) => (
<DropdownBS.Item as="li" className={styles['option-item']} key={option}>
<FormBS.Check
type="checkbox"
value={option}
label={option}
onChange={handleCheck}
id={`check-item-${option}`}
checked={selectedOptions.includes(option)}
className={styles['option-item__checkbox-input']}
/>
</DropdownBS.Item>
))}

{noOptionsFound && (
<DropdownBS.Item as="li" disabled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ type SelectMultipleActions =
payload: string
}
| {
type: 'TOGGLE_ALL_OPTIONS'
type: 'SELECT_ALL_OPTIONS'
}
| {
type: 'DESELECT_ALL_OPTIONS'
}
| {
type: 'SEARCH'
Expand All @@ -44,19 +47,32 @@ export const selectMultipleReducer = (
...state,
selectedOptions: state.selectedOptions.filter((option) => option !== action.payload)
}
case 'TOGGLE_ALL_OPTIONS':
case 'SELECT_ALL_OPTIONS':
return {
...state,
selectedOptions:
state.filteredOptions.length > 0
? Array.from(new Set([...state.selectedOptions, ...state.filteredOptions]))
: state.options
}
case 'DESELECT_ALL_OPTIONS':
return {
...state,
selectedOptions: state.selectedOptions.length === state.options.length ? [] : state.options
selectedOptions:
state.filteredOptions.length > 0
? state.selectedOptions.filter((option) => !state.filteredOptions.includes(option))
: []
}

case 'SEARCH':
return {
...state,
filteredOptions: action.payload
? state.options.filter((option) =>
option.toLowerCase().includes(action.payload.toLowerCase())
)
: state.options,
filteredOptions:
action.payload !== ''
? state.options.filter((option) =>
option.toLowerCase().includes(action.payload.toLowerCase())
)
: [],
searchValue: action.payload
}
default:
Expand All @@ -74,8 +90,12 @@ export const removeOption = /* istanbul ignore next */ (option: string): SelectM
payload: option
})

export const toggleAllOptions = /* istanbul ignore next */ (): SelectMultipleActions => ({
type: 'TOGGLE_ALL_OPTIONS'
export const selectAllOptions = /* istanbul ignore next */ (): SelectMultipleActions => ({
type: 'SELECT_ALL_OPTIONS'
})

export const deselectAllOptions = /* istanbul ignore next */ (): SelectMultipleActions => ({
type: 'DESELECT_ALL_OPTIONS'
})

export const searchOptions = /* istanbul ignore next */ (value: string): SelectMultipleActions => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('SelectMultiple', () => {
cy.get('@onChange').should('not.have.been.called')
})

it('selects an option is shown as selected both in the menu as well as in the selected options', () => {
it('should select an option and be shown as selected both in the menu as well as in the selected options', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('SelectMultiple', () => {
})
})

it('selects all options when the toggle all checkbox is checked while been unchecked before', () => {
it('selects all options', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('SelectMultiple', () => {
cy.findByText('Select...').should('not.exist')
})

it('deselects all options when the toggle all checkbox is unchecked while been checked before', () => {
it('deselects all options', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
Expand All @@ -176,6 +176,84 @@ describe('SelectMultiple', () => {
cy.findByText('Select...').should('exist')
})

it('should select all filtered options', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
/>
)
cy.clock()

cy.findByLabelText('Toggle options menu').click()
cy.findByPlaceholderText('Search...').type('Read')

cy.tick(SELECT_MENU_SEARCH_DEBOUNCE_TIME)

cy.findByLabelText('Reading').should('exist')
cy.findByLabelText('Swimming').should('not.exist')
cy.findByLabelText('Running').should('not.exist')
cy.findByLabelText('Cycling').should('not.exist')
cy.findByLabelText('Cooking').should('not.exist')
cy.findByLabelText('Gardening').should('not.exist')

cy.findByLabelText('Toggle all options').click()

cy.findByLabelText('List of selected options')
.should('exist')
.within(() => {
cy.findByText('Reading').should('exist')
cy.findByText('Swimming').should('not.exist')
cy.findByText('Running').should('not.exist')
cy.findByText('Cycling').should('not.exist')
cy.findByText('Cooking').should('not.exist')
cy.findByText('Gardening').should('not.exist')
})
cy.findByText('Select...').should('not.exist')
})

it('should unselect only filtered options', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
/>
)
cy.clock()

cy.findByLabelText('Toggle options menu').click()
cy.findByLabelText('Toggle all options').click()

cy.findByLabelText('Reading').should('be.checked')
cy.findByLabelText('Swimming').should('be.checked')
cy.findByLabelText('Running').should('be.checked')
cy.findByLabelText('Cycling').should('be.checked')
cy.findByLabelText('Cooking').should('be.checked')
cy.findByLabelText('Gardening').should('be.checked')

cy.findByPlaceholderText('Search...').type('Read')

cy.tick(SELECT_MENU_SEARCH_DEBOUNCE_TIME)

cy.findByLabelText('Reading').should('exist')
cy.findByLabelText('Swimming').should('not.exist')
cy.findByLabelText('Running').should('not.exist')
cy.findByLabelText('Cycling').should('not.exist')
cy.findByLabelText('Cooking').should('not.exist')
cy.findByLabelText('Gardening').should('not.exist')

cy.findByLabelText('Toggle all options').click()

cy.findByLabelText('List of selected options')
.should('exist')
.within(() => {
cy.findByText('Reading').should('not.exist')
cy.findByText('Swimming').should('exist')
cy.findByText('Running').should('exist')
cy.findByText('Cycling').should('exist')
cy.findByText('Cooking').should('exist')
cy.findByText('Gardening').should('exist')
})
})

it('should show correct filtered options when searching for a value', () => {
cy.mount(
<SelectMultiple
Expand Down Expand Up @@ -241,7 +319,7 @@ describe('SelectMultiple', () => {
cy.findByText('2 selected').should('exist')
})

it('should show No Options Found when search does not match any option', () => {
it('should show No Options Found and toggle all chebox be disabled when search does not match any option', () => {
cy.mount(
<SelectMultiple
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
Expand All @@ -252,6 +330,7 @@ describe('SelectMultiple', () => {
cy.findByPlaceholderText('Search...').type('Yoga')

cy.findByText('No options found').should('exist')
cy.findByLabelText('Toggle all options').should('be.disabled')
})

it('should be disabled when isDisabled is true', () => {
Expand Down
Loading

0 comments on commit 5a12f28

Please sign in to comment.