Skip to content

Commit

Permalink
feat: [M3-7665] - RegionMultiSelect Component (linode#10084)
Browse files Browse the repository at this point in the history
* RegionMultiSelect

* RegionMultiSelect Story

* Added changeset: RegionMultiSelect Component

* unit test - RegionMultiSelect

* PR - feedback @jaalah-akamai @abailly-akamai

* Add border to Select / Deselect All option

* Extract RenderOption as a Separate Component in RegionMultiSelect

* Use RegionOption in RegionSelect

* RegionMultiSelect -Test coverage

* Update packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx

Co-authored-by: Alban Bailly <[email protected]>

---------

Co-authored-by: Alban Bailly <[email protected]>
  • Loading branch information
cpathipa and abailly-akamai authored Jan 23, 2024
1 parent b84275a commit c432177
Show file tree
Hide file tree
Showing 10 changed files with 655 additions and 76 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10084-added-1705676104397.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

RegionMultiSelect Component ([#10084](https://github.com/linode/manager/pull/10084))
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from 'react';

import { regions } from 'src/__data__/regionsData';
import { Box } from 'src/components/Box';
import { Flag } from 'src/components/Flag';
import {
RemovableItem,
RemovableSelectionsList,
} from 'src/components/RemovableSelectionsList/RemovableSelectionsList';
import { sortByString } from 'src/utilities/sort-by';

import { RegionMultiSelect } from './RegionMultiSelect';
import { StyledFlagContainer } from './RegionSelect.styles';

import type { RegionMultiSelectProps } from './RegionSelect.types';
import type { Meta, StoryObj } from '@storybook/react';
import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types';

interface SelectedRegionsProps {
onRemove: (data: RegionSelectOption) => void;
selectedRegions: RegionSelectOption[];
}

interface LabelComponentProps {
selection: RemovableItem;
}

const sortRegionOptions = (a: RegionSelectOption, b: RegionSelectOption) => {
return sortByString(a.label, b.label, 'asc');
};

const LabelComponent = ({ selection }: LabelComponentProps) => {
return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexGrow: 1,
}}
>
<StyledFlagContainer>
<Flag country={selection.data.country} />
</StyledFlagContainer>
{selection.label}
</Box>
);
};

const SelectedRegionsList = ({
onRemove,
selectedRegions,
}: SelectedRegionsProps) => {
const handleRemove = (item: RemovableItem) => {
onRemove(item.data);
};

return (
<RemovableSelectionsList
selectionData={selectedRegions.map((item, index) => {
return { ...item, id: index };
})}
LabelComponent={LabelComponent}
headerText=""
noDataText=""
onRemove={handleRemove}
/>
);
};

export const Default: StoryObj<RegionMultiSelectProps> = {
render: (args) => {
const SelectWrapper = () => {
const [selectedRegionsIds, setSelectedRegionsIds] = useState<string[]>(
[]
);

const handleSelectionChange = (selectedIds: string[]) => {
setSelectedRegionsIds(selectedIds);
};

return (
<Box sx={{ minHeight: 500 }}>
<RegionMultiSelect
{...args}
handleSelection={handleSelectionChange}
selectedIds={selectedRegionsIds}
/>
</Box>
);
};

return SelectWrapper();
},
};

const meta: Meta<RegionMultiSelectProps> = {
args: {
SelectedRegionsList,
currentCapability: 'Linodes',
disabled: false,
errorText: '',
isClearable: false,
label: 'Regions',
placeholder: 'Select Regions or type to search',
regions,
sortRegionOptions,
},
component: RegionMultiSelect,
title: 'Components/Selects/Region Multi Select',
};
export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';

import { regionFactory } from 'src/factories/regions';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { RegionMultiSelect } from './RegionMultiSelect';

import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types';

const regions = regionFactory.buildList(1, {
id: 'us-east',
label: 'Newark, NJ',
});

const regionsNewark = regionFactory.buildList(1, {
id: 'us-east',
label: 'Newark, NJ',
});
const regionsAtlanta = regionFactory.buildList(1, {
id: 'us-southeast',
label: 'Atlanta, GA',
});
interface SelectedRegionsProps {
onRemove: (data: RegionSelectOption) => void;
selectedRegions: RegionSelectOption[];
}
const SelectedRegionsList = ({
onRemove,
selectedRegions,
}: SelectedRegionsProps) => (
<ul>
{selectedRegions.map((region, index) => (
<li aria-label={region.label} key={index}>
{region.label}
<button onClick={() => onRemove(region)}>Remove</button>
</li>
))}
</ul>
);

const mockHandleSelection = vi.fn();

describe('RegionMultiSelect', () => {
it('renders correctly with initial props', () => {
renderWithTheme(
<RegionMultiSelect
currentCapability="Block Storage"
handleSelection={mockHandleSelection}
regions={regions}
selectedIds={[]}
/>
);

screen.getByRole('combobox', { name: 'Regions' });
});

it('should be able to select all the regions correctly', () => {
renderWithTheme(
<RegionMultiSelect
currentCapability="Block Storage"
handleSelection={mockHandleSelection}
regions={[...regionsNewark, ...regionsAtlanta]}
selectedIds={[]}
/>
);

// Open the dropdown
fireEvent.click(screen.getByRole('button', { name: 'Open' }));

fireEvent.click(screen.getByRole('option', { name: 'Select All' }));

// Check if all the option is selected
expect(
screen.getByRole('option', {
name: 'Newark, NJ (us-east)',
})
).toHaveAttribute('aria-selected', 'true');
expect(
screen.getByRole('option', {
name: 'Newark, NJ (us-east)',
})
).toHaveAttribute('aria-selected', 'true');
});

it('should be able to deselect all the regions', () => {
renderWithTheme(
<RegionMultiSelect
currentCapability="Block Storage"
handleSelection={mockHandleSelection}
regions={[...regionsNewark, ...regionsAtlanta]}
selectedIds={['us-east', 'us-southeast']}
/>
);

// Open the dropdown
fireEvent.click(screen.getByRole('button', { name: 'Open' }));

fireEvent.click(screen.getByRole('option', { name: 'Deselect All' }));

// Check if all the option is deselected selected
expect(
screen.getByRole('option', {
name: 'Newark, NJ (us-east)',
})
).toHaveAttribute('aria-selected', 'false');
expect(
screen.getByRole('option', {
name: 'Newark, NJ (us-east)',
})
).toHaveAttribute('aria-selected', 'false');
});

it('should render selected regions correctly', () => {
renderWithTheme(
<RegionMultiSelect
SelectedRegionsList={({ onRemove, selectedRegions }) => (
<SelectedRegionsList
onRemove={onRemove}
selectedRegions={selectedRegions}
/>
)}
currentCapability="Block Storage"
handleSelection={mockHandleSelection}
regions={[...regionsNewark, ...regionsAtlanta]}
selectedIds={[]}
/>
);

// Open the dropdown
fireEvent.click(screen.getByRole('button', { name: 'Open' }));

fireEvent.click(screen.getByRole('option', { name: 'Select All' }));

// Close the dropdown
fireEvent.click(screen.getByRole('button', { name: 'Close' }));

// Check if all the options are rendered
expect(
screen.getByRole('listitem', {
name: 'Newark, NJ (us-east)',
})
).toBeInTheDocument();
expect(
screen.getByRole('listitem', {
name: 'Newark, NJ (us-east)',
})
).toBeInTheDocument();
});
});
Loading

0 comments on commit c432177

Please sign in to comment.