forked from linode/manager
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [M3-7665] - RegionMultiSelect Component (linode#10084)
* 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
1 parent
b84275a
commit c432177
Showing
10 changed files
with
655 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
111 changes: 111 additions & 0 deletions
111
packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
150 changes: 150 additions & 0 deletions
150
packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.