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

chore(SingleSelect): SingleSelect - Controlled Component #319

Merged
merged 9 commits into from
Apr 4, 2024
26 changes: 24 additions & 2 deletions src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Select } from '~/src/index';
import { useState } from 'react';
import { Select, SelectSingle } from '~/src/index';
import type { SelectOption, SelectProperties } from './Select';
import { SingleSelectOptions } from './testUtils';

const meta: Meta<typeof Select> = {
const meta: Meta<typeof SelectSingle> = {
title: 'Components (Draft)/Selects/Single select',
tags: ['autodocs'],
component: Select,
Expand All @@ -16,7 +18,24 @@ export default meta;

type Story = StoryObj<typeof meta>;

function SelectWrapper({ ...arguments_ }: SelectProperties): JSX.Element {
const [selected, setSelected] = useState<string>('');

const onHandleChange = (
newValue: SelectOption | SelectOption[] | undefined
shindigira marked this conversation as resolved.
Show resolved Hide resolved
): void => {
// Just to resolve TypeScript since we are using Select in single format
if (Array.isArray(newValue)) return;
shindigira marked this conversation as resolved.
Show resolved Hide resolved
setSelected(newValue?.value ?? '');
};

return (
<SelectSingle {...arguments_} value={selected} onChange={onHandleChange} />
);
}

export const SingleSelect: Story = {
render: _arguments => SelectWrapper(_arguments),
name: 'Enabled',
args: {
id: 'singleSelect',
Expand All @@ -26,6 +45,7 @@ export const SingleSelect: Story = {
};

export const SingleSelectHover: Story = {
render: _arguments => SelectWrapper(_arguments),
name: 'Hover',
args: {
id: 'singleSelect',
Expand All @@ -36,6 +56,7 @@ export const SingleSelectHover: Story = {
};

export const SingleSelectFocus: Story = {
render: _arguments => SelectWrapper(_arguments),
name: 'Focus',
args: {
id: 'singleSelect',
Expand All @@ -46,6 +67,7 @@ export const SingleSelectFocus: Story = {
};

export const SingleSelectDisabled: Story = {
render: _arguments => SelectWrapper(_arguments),
name: 'Disabled',
args: {
id: 'singleSelect',
Expand Down
57 changes: 35 additions & 22 deletions src/components/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,48 @@ import { jest } from '@storybook/jest';
import '@testing-library/jest-dom';
import { act, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Select } from './Select';
import { useState } from 'react';
import { Select, SelectOption } from './Select';
import { MultipleSelectOptions, SingleSelectOptions } from './testUtils';

const SingleSelectWrapper = (): JSX.Element => {
const [selectedValue, setSelectedValue] = useState<string>(
SingleSelectOptions[0].value
);

const onHandleChange = (
newValue: SelectOption | SelectOption[] | undefined
): void => {
// Just to resolve TypeScript since we are using Select in single format
if (Array.isArray(newValue)) return;
setSelectedValue(newValue?.value ?? '');
};

return (
<Select
id='single'
options={SingleSelectOptions}
onChange={onHandleChange}
value={selectedValue}
/>
);
};

describe('<SelectSingle />', () => {
it('renders Single select with default value', () => {
render(<Select id='single' options={SingleSelectOptions} />);
expect(screen.getByRole('combobox')).toHaveValue('option1');
expect(screen.getByRole('option', { name: 'Option 1' }).selected).toBe(
true
);
// render(<Select id='single' options={SingleSelectOptions} />);
render(<SingleSelectWrapper />);

const selectElement = screen.getByRole('combobox');
expect(selectElement).toHaveValue('option1');
});

it('Handles Single selection change', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

render(
<Select
id='single-change'
label='Single Select'
options={SingleSelectOptions}
defaultValue='option1'
onChange={onChange}
/>
);
render(<SingleSelectWrapper />);
const selectElement = screen.getByRole('combobox');

await user.selectOptions(screen.getByRole('combobox'), 'option3');
await userEvent.selectOptions(selectElement, 'option3');
expect(screen.getByRole('combobox')).toHaveValue('option3');
expect(onChange).toHaveBeenCalledWith(SingleSelectOptions[2]);
});
});

Expand Down Expand Up @@ -73,8 +86,8 @@ describe('<SelectMulti />', () => {

// Change handler is called with the expected content
expect(onChange).toHaveBeenCalledWith([
{ ...MultipleSelectOptions[0], selected: true },
{ ...MultipleSelectOptions[3], selected: true }
{ ...MultipleSelectOptions[1], selected: true },
{ ...MultipleSelectOptions[4], selected: true }
]);

// Tags are rendered for the selected options
Expand Down
7 changes: 6 additions & 1 deletion src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SelectHTMLAttributes } from 'react';
import { SelectMulti } from './SelectMulti';
import { SelectSingle } from './SelectSingle';

Expand All @@ -7,14 +8,18 @@ export interface SelectOption {
selected?: boolean;
}

export interface SelectProperties {
export interface SelectProperties
extends SelectHTMLAttributes<HTMLSelectElement> {
disabled?: boolean;
id: string;
isMulti?: boolean;
label?: string;
onChange?: (selected: SelectOption | SelectOption[] | undefined) => void;
options: SelectOption[];
maxSelections?: number;
className?: string;
value?: string;
defaultOptionLabel?: string;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/components/Select/SelectMulti.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const SelectMulti = ({
options,
label,
onChange = noOp,
defaultOptionLabel = '-- select an option --',
maxSelections = MAX_SELECTIONS,
...properties
}: SelectProperties): JSX.Element => {
Expand Down Expand Up @@ -67,7 +68,7 @@ export const SelectMulti = ({
data-open
{...properties}
>
{buildOptions(options)}
{buildOptions(options, defaultOptionLabel)}
</select>
</div>
);
Expand Down
12 changes: 10 additions & 2 deletions src/components/Select/SelectSingle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const SelectSingle = ({
label,
onChange = noOp,
maxSelections,
value = '',
defaultOptionLabel = '-- select an option --',
...properties
}: SelectProperties): JSX.Element => {
const onSelect = (
Expand All @@ -25,8 +27,14 @@ export const SelectSingle = ({
{label}
</label>
<div className='a-select'>
<select id={id} data-testid={id} {...properties} onChange={onSelect}>
{buildOptions(options)}
<select
id={id}
data-testid={id}
{...properties}
onChange={onSelect}
value={value}
>
{buildOptions(options, defaultOptionLabel)}
</select>
</div>
</>
Expand Down
16 changes: 13 additions & 3 deletions src/components/Select/selectUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import type { SelectOption } from './Select';

/* Convert an SelectOption[] into HTMLOptionElement[] */
export const buildOptions = (options: SelectOption[]): JSX.Element[] => {
export const buildOptions = (
options: SelectOption[],
defaultOptionLabel: string
): JSX.Element[] => {
if (options.length === 0) return [];

return options.map(({ value, label }: SelectOption) => (
const formattedOptions = options.map(({ value, label }: SelectOption) => (
<option key={value} value={value}>
{label}
</option>
));

return [
<option key='initial' disabled value=''>
{defaultOptionLabel}
</option>,
...formattedOptions
];
};

/* Map a value to it's corresponding Option */
export const findOptionByValue = (
options: SelectOption[],
value: string
): SelectOption | undefined => options.find(opt => opt.value === value);
): SelectOption | undefined => options.find(opt => opt.value === value);