Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): add an option to not allow deselecting a selected option #225

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/BoemlyFormControl/BoemlyFormControl.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ SelectWithDisabledOption.args = {
],
};

export const SelectWithPreventDeselection = Template.bind({});
SelectWithPreventDeselection.args = {
id: 'select',
inputType: 'Select',
selectOptions: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
],
selectProps: { preventDeselection: true },
};

export const Checkbox = Template.bind({});
Checkbox.args = {
id: 'checkbox',
Expand Down
7 changes: 7 additions & 0 deletions src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ WithPlaceholder.args = {
options: commonOptions,
};

export const WithPreventDeselection = Template.bind({});
WithPreventDeselection.args = {
color: 'black',
preventDeselection: true,
options: commonOptions,
};

export const Searchable = Template.bind({});
Searchable.args = {
isSearchable: true,
Expand Down
113 changes: 113 additions & 0 deletions src/components/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,117 @@ describe('The Select component', () => {
// Check if onClose was called
expect(onCloseMock).toHaveBeenCalledTimes(1);
});

it('allows deselection when preventDeselection is false', () => {
const handleChange = jest.fn();

render(
<Select color="black" options={mockOptions} isMultiple={false} onChange={handleChange} />
);

const toggleButton = screen.getByRole('combobox');
fireEvent.click(toggleButton);

const option1 = screen.getByText('Option 1');
fireEvent.click(option1);

expect(handleChange).toHaveBeenCalledWith(['1']);
fireEvent.click(toggleButton);
// Click the same option again to deselect
fireEvent.click(option1);

expect(handleChange).toHaveBeenCalledWith(['1']);
});

it('prevents deselection when preventDeselection is true', () => {
const handleChange = jest.fn();

render(
<Select
color="black"
options={mockOptions}
isMultiple={false}
preventDeselection={true}
onChange={handleChange}
/>
);

fireEvent.click(screen.getByRole('combobox'));
fireEvent.click(screen.getByText('Option 1'));

expect(handleChange).toHaveBeenCalledWith(['1']);

fireEvent.click(screen.getByRole('combobox'));
fireEvent.click(screen.getByRole('menuitemradio', { name: 'Option 1' })); // try to deselect

// check that the text in the combobox doesn't change
expect(screen.getByRole('combobox')).toHaveTextContent('Option 1');
});

it('allows switching between options when preventDeselection is true', () => {
const handleChange = jest.fn();

render(
<Select
color="black"
options={mockOptions}
isMultiple={false}
preventDeselection={true}
onChange={handleChange}
/>
);

const toggleButton = screen.getByRole('combobox');
fireEvent.click(toggleButton);

const option1 = screen.getByText('Option 1');

fireEvent.click(option1);
expect(handleChange).toHaveBeenCalledWith(['1']);

expect(screen.getByRole('combobox')).toHaveTextContent('Option 1');

// re open dropdown and select another option
fireEvent.click(toggleButton);

const option2 = screen.getByText('Option 2');

fireEvent.click(option2);
expect(handleChange).toHaveBeenCalledWith(['2']);
// check that the text in the combobox did change
expect(screen.getByRole('combobox')).toHaveTextContent('Option 2');
});

it('handles disabled options correctly', () => {
const handleChange = jest.fn();

const optionsWithDisabled = [
{ label: 'Option 1', value: '1', disabled: true },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];

render(
<Select
color="black"
options={optionsWithDisabled}
isMultiple={false}
onChange={handleChange}
/>
);

const toggleButton = screen.getByRole('combobox');
fireEvent.click(toggleButton);

const disabledOption = screen.getByText('Option 1');
const enabledOption = screen.getByText('Option 2');

// try to select disabled option
fireEvent.click(disabledOption);
expect(handleChange).not.toHaveBeenCalled();

// Select enabled option
fireEvent.click(enabledOption);
expect(handleChange).toHaveBeenCalledWith(['2']);
});
});
22 changes: 19 additions & 3 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface BoemlySelectProps extends Omit<SelectProps, 'onChange' | 'value
isFullWidth?: boolean;
isSearchable?: boolean;
isMultiple?: boolean;
preventDeselection?: boolean;
selectAllText?: string;
value?: string[];
size?: 'xs' | 'sm' | 'md' | 'lg';
Expand All @@ -60,6 +61,7 @@ export const BoemlySelect: React.FC<BoemlySelectProps> = ({
isFullWidth = true,
isSearchable = false,
isMultiple = false,
preventDeselection = false,
selectAllText = 'Select All',
size = 'md',
variant = 'outline',
Expand Down Expand Up @@ -110,8 +112,22 @@ export const BoemlySelect: React.FC<BoemlySelectProps> = ({
? prevSelectedOptions.filter((val) => val !== optionValue)
: [...prevSelectedOptions, optionValue];
} else {
const isOptionSelected = prevSelectedOptions.includes(optionValue);
newSelectedOptions = isOptionSelected ? [] : [optionValue]; // Deselect if selected, otherwise select
// If preventDeselection is true, don't allow deselection of the selected option
if (preventDeselection) {
if (prevSelectedOptions.length === 0) {
// If no option is selected, select the clicked option
newSelectedOptions = [optionValue];
} else if (prevSelectedOptions[0] === optionValue) {
// If the clicked option is the same as the current selection, keep the selection and don't deselect
newSelectedOptions = prevSelectedOptions;
} else {
// If a different option is clicked, select the new option
newSelectedOptions = [optionValue];
}
} else {
const isOptionSelected = prevSelectedOptions.includes(optionValue);
newSelectedOptions = isOptionSelected ? [] : [optionValue]; // Deselect if selected, otherwise select
}
setIsOpen(false); // Close dropdown for single select
}

Expand All @@ -121,7 +137,7 @@ export const BoemlySelect: React.FC<BoemlySelectProps> = ({

setSearchTerm(''); // Clear search term after selection
},
[isMultiple, onChange]
[isMultiple, onChange, preventDeselection]
);

const onSelectAll = useCallback(() => {
Expand Down
Loading