Skip to content
Draft
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
5 changes: 4 additions & 1 deletion assets/js/common/MultiSelect/MultiSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const defaultClassNames = {
function MultiSelect({
options,
values,
value,
isMulti = true,
disabled = false,
components = defaultComponents,
selectClassNames = defaultClassNames,
Expand All @@ -88,7 +90,8 @@ function MultiSelect({
}) {
return (
<Select
isMulti
isMulti={isMulti}
value={value}
defaultValue={values}
options={options}
classNames={selectClassNames}
Expand Down
46 changes: 46 additions & 0 deletions assets/js/common/SearchableSelect/SearchableSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import classNames from 'classnames';
import MultiSelect from '@common/MultiSelect';

const defaultFilterOption = (option, rawInput) => {
if (!rawInput) return true;
const input = rawInput.toLowerCase();
return (
option.data.searchLabel?.toLowerCase().includes(input) ||
option.data.label.toLowerCase().includes(input)
);
};

function SearchableSelect({
value,
options,
onChange,
disabled = false,
className = '',
isClearable = false,
placeholder,
noOptionsMessage,
filterOption = defaultFilterOption,
...props
}) {
const selectedOption = options.find((opt) => opt.value === value);

return (
<MultiSelect
options={options}
value={selectedOption}
isMulti={false}
onChange={(option) => onChange(option?.value)}
disabled={disabled}
isClearable={isClearable}
isSearchable
className={classNames('text-sm', className)}
placeholder={placeholder}
noOptionsMessage={noOptionsMessage}
filterOption={filterOption}
{...props}
/>
);
}

export default SearchableSelect;
90 changes: 90 additions & 0 deletions assets/js/common/SearchableSelect/SearchableSelect.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import SearchableSelect from '.';

export default {
title: 'Components/SearchableSelect',
component: SearchableSelect,
argTypes: {
options: {
type: 'array',
description: 'The list of options to render',
control: {
type: 'array',
},
},
value: {
type: 'string',
description: 'Selected option value',
control: {
type: 'text',
},
},
disabled: {
type: 'boolean',
description: 'Component is disabled or not',
control: {
type: 'boolean',
},
},
isClearable: {
type: 'boolean',
description: 'Enable clear action',
control: {
type: 'boolean',
},
},
onChange: {
description: 'A function to be called when selected option changes',
table: {
type: { summary: '(value) => {}' },
},
},
},
};

const options = [
{
value: 'Europe/Berlin',
label: 'Europe/Berlin (GMT+1)',
searchLabel: 'Europe/Berlin',
},
{
value: 'America/New_York',
label: 'America/New_York (GMT-5)',
searchLabel: 'America/New_York',
},
{
value: 'Asia/Tokyo',
label: 'Asia/Tokyo (GMT+9)',
searchLabel: 'Asia/Tokyo',
},
];

export const Default = {
args: {
options,
className: 'w-96',
placeholder: 'Select timezone...',
noOptionsMessage: () => 'No options found',
},
};

export const WithValue = {
args: {
...Default.args,
value: 'Europe/Berlin',
},
};

export const Clearable = {
args: {
...WithValue.args,
isClearable: true,
},
};

export const Disabled = {
args: {
...WithValue.args,
disabled: true,
},
};
75 changes: 75 additions & 0 deletions assets/js/common/SearchableSelect/SearchableSelect.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';

import SearchableSelect from '.';

const options = [
{
value: 'Europe/Berlin',
label: 'Europe/Berlin (GMT+1)',
searchLabel: 'Europe/Berlin',
},
{
value: 'America/New_York',
label: 'America/New_York (GMT-5)',
searchLabel: 'America/New_York',
},
{
value: 'Asia/Tokyo',
label: 'Asia/Tokyo (GMT+9)',
searchLabel: 'Asia/Tokyo',
},
];

describe('SearchableSelect Component', () => {
it('should render selected value', () => {
render(
<SearchableSelect
options={options}
value="Europe/Berlin"
onChange={jest.fn()}
placeholder="Select timezone..."
/>
);

expect(screen.getByText('Europe/Berlin (GMT+1)')).toBeVisible();
});

it('should call onChange with option value', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

render(
<SearchableSelect
options={options}
onChange={onChange}
placeholder="Select timezone..."
/>
);

await user.click(screen.getByText('Select timezone...'));
await user.click(screen.getByText('Asia/Tokyo (GMT+9)'));

expect(onChange).toHaveBeenCalledWith('Asia/Tokyo');
});

it('should filter options by searchLabel by default', async () => {
const user = userEvent.setup();

render(
<SearchableSelect
options={options}
onChange={jest.fn()}
placeholder="Select timezone..."
/>
);

await user.click(screen.getByText('Select timezone...'));
await user.type(screen.getByRole('combobox'), 'new_york');

expect(screen.getByText('America/New_York (GMT-5)')).toBeVisible();
expect(screen.queryByText('Asia/Tokyo (GMT+9)')).not.toBeInTheDocument();
});
});
3 changes: 3 additions & 0 deletions assets/js/common/SearchableSelect/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SearchableSelect from './SearchableSelect';

export default SearchableSelect;
Loading