Skip to content

Commit 6787004

Browse files
Implement PBL store selector internal component
1 parent 17ebb3a commit 6787004

File tree

15 files changed

+387
-3
lines changed

15 files changed

+387
-3
lines changed

src/components/internal/FormFields/Select/Select.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const Select = <T extends SelectItem>({
3030
placeholder,
3131
uniqueId,
3232
renderListItem,
33+
renderButtonContent,
3334
isCollatingErrors,
3435
setToTargetWidth,
3536
withoutCollapseIndicator = false,
@@ -330,6 +331,7 @@ const Select = <T extends SelectItem>({
330331
multiSelect={multiSelect}
331332
placeholder={placeholder}
332333
readonly={readonly}
334+
renderButtonContent={renderButtonContent}
333335
selectListId={selectListId}
334336
showList={showList}
335337
toggleButtonRef={toggleButtonRef}

src/components/internal/FormFields/Select/components/SelectButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ const SelectButton = <T extends SelectItem>(props: SelectButtonProps<T> & { appl
8787
id={props.id}
8888
{...(showList && filterable ? {} : { 'aria-label': props['aria-label'], 'aria-labelledby': props['aria-labelledby'] })}
8989
>
90-
{showList && filterable ? (
90+
{props.renderButtonContent ? (
91+
props.renderButtonContent({ item: buttonActiveItem })
92+
) : showList && filterable ? (
9193
<input
9294
aria-autocomplete="list"
9395
aria-label={props['aria-label']}

src/components/internal/Icon/Icon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const icons = {
1212
'chevron-left': () => import('../../../images/icons/chevron-left.svg?component'),
1313
'chevron-right': () => import('../../../images/icons/chevron-right.svg?component'),
1414
'chevron-up': () => import('../../../images/icons/chevron-up.svg?component'),
15+
'chevron-up-down': () => import('../../../images/icons/chevron-up-down.svg?component'),
1516
copy: () => import('../../../images/icons/copy.svg?component'),
1617
'cross-circle-fill': () => import('../../../images/icons/cross-circle-fill.svg?component'),
1718
cross: () => import('../../../images/icons/cross.svg?component'),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { render, screen } from '@testing-library/preact';
5+
import { beforeEach, describe, test, expect, vi } from 'vitest';
6+
import { StoreSelector } from './StoreSelector';
7+
import { STORES } from '../../../../mocks/mock-data';
8+
9+
const mockStores = STORES.map(({ storeCode, description }) => ({ id: storeCode, name: storeCode, storeCode, description }));
10+
11+
describe('StoreSelector', () => {
12+
const mockSetSelectedStoreId = vi.fn();
13+
14+
beforeEach(() => {
15+
mockSetSelectedStoreId.mockClear();
16+
});
17+
18+
test('should render null when stores array is empty', () => {
19+
const { container } = render(<StoreSelector stores={[]} selectedStoreId={undefined} setSelectedStoreId={mockSetSelectedStoreId} />);
20+
expect(container.firstChild).toBeNull();
21+
});
22+
23+
test('should not render a button when there is only one store', () => {
24+
render(<StoreSelector stores={mockStores.slice(0, 1)} selectedStoreId={mockStores[0]?.id} setSelectedStoreId={mockSetSelectedStoreId} />);
25+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
26+
});
27+
28+
test('should render Select component when there are multiple stores', () => {
29+
render(<StoreSelector stores={mockStores} selectedStoreId={mockStores[0]?.id} setSelectedStoreId={mockSetSelectedStoreId} />);
30+
31+
expect(screen.getByRole('button')).toBeInTheDocument();
32+
});
33+
34+
test('should render with correct props passed to Select component', () => {
35+
render(<StoreSelector stores={mockStores} selectedStoreId={mockStores[1]?.id} setSelectedStoreId={mockSetSelectedStoreId} />);
36+
37+
const button = screen.getByRole('button');
38+
expect(button).toBeInTheDocument();
39+
40+
expect(button).toHaveTextContent('STORE_LON_001');
41+
expect(button).toHaveTextContent('Main Store - London');
42+
});
43+
44+
test('should render button content correctly when data.item exists', () => {
45+
render(<StoreSelector stores={mockStores} selectedStoreId={mockStores[0]?.id} setSelectedStoreId={mockSetSelectedStoreId} />);
46+
47+
expect(screen.getByText('STORE_NY_001')).toBeInTheDocument();
48+
expect(screen.getByText('Main Store - New York')).toBeInTheDocument();
49+
});
50+
51+
test('should pass correct props to Select component', () => {
52+
render(<StoreSelector stores={mockStores} selectedStoreId={mockStores[0]?.id} setSelectedStoreId={mockSetSelectedStoreId} />);
53+
54+
const button = screen.getByRole('button');
55+
expect(button).toBeInTheDocument();
56+
57+
expect(button).toHaveTextContent('STORE_NY_001');
58+
expect(button).toHaveTextContent('Main Store - New York');
59+
});
60+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useCallback, useEffect, useState } from 'preact/hooks';
2+
import Select from '../FormFields/Select';
3+
import { StoreSelectorButtonContent } from './StoreSelectorButton/StoreSelectorButton';
4+
import { StoreSelectorItem } from './StoreSelectorItem/StoreSelectorItem';
5+
import { StoreSelectorItemParams, StoreSelectorProps } from './types';
6+
import { SelectChangeEvent } from '../FormFields/Select/types';
7+
import { containerQueries, useResponsiveContainer } from '../../../hooks/useResponsiveContainer';
8+
9+
export const StoreSelector = ({ stores, selectedStoreId, setSelectedStoreId }: StoreSelectorProps) => {
10+
const isMobileContainer = useResponsiveContainer(containerQueries.down.xs);
11+
const handleStoreChange = useCallback(
12+
({ target }: SelectChangeEvent) => {
13+
setSelectedStoreId(target.value);
14+
},
15+
[setSelectedStoreId]
16+
);
17+
18+
const renderButtonContent = (data: { item?: StoreSelectorItemParams }) => {
19+
if (!data.item) {
20+
return null;
21+
}
22+
return <StoreSelectorButtonContent name={data.item.storeCode} description={data.item.description} />;
23+
};
24+
25+
if (stores && stores.length === 1) {
26+
return <StoreSelectorItem name={stores[0]?.storeCode} description={stores[0]?.description} />;
27+
}
28+
29+
const canRenderSelector = stores && stores.length > 1;
30+
if (!canRenderSelector) return null;
31+
32+
return (
33+
<Select
34+
filterable={false}
35+
items={stores}
36+
multiSelect={false}
37+
onChange={handleStoreChange}
38+
renderButtonContent={renderButtonContent}
39+
renderListItem={data => <StoreSelectorItem name={data.item.name} description={data.item.description} />}
40+
selected={selectedStoreId}
41+
setToTargetWidth={isMobileContainer}
42+
showOverlay={false}
43+
withoutCollapseIndicator={true}
44+
/>
45+
);
46+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@use '../../../../style';
2+
3+
.adyen-pe-store-selector-button {
4+
align-items: center;
5+
display: flex;
6+
flex-direction: row;
7+
justify-content: space-between;
8+
width: 100%;
9+
10+
&__labels {
11+
display: flex;
12+
flex-direction: column;
13+
overflow: hidden;
14+
white-space: nowrap;
15+
}
16+
17+
&__name,
18+
&__description {
19+
overflow: hidden;
20+
text-align: left;
21+
text-overflow: ellipsis;
22+
white-space: nowrap;
23+
}
24+
25+
&__description {
26+
color: style.token(color-label-secondary);
27+
}
28+
29+
&__icon {
30+
font-size: 16px;
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Typography from '../../Typography/Typography';
2+
import { TypographyElement, TypographyVariant } from '../../Typography/types';
3+
import Icon from '../../Icon';
4+
import './StoreSelectorButton.scss';
5+
6+
interface StoreSelectorButtonContentProps {
7+
name?: string;
8+
description?: string;
9+
}
10+
11+
export const StoreSelectorButtonContent = ({ name, description }: StoreSelectorButtonContentProps) => (
12+
<div className="adyen-pe-store-selector-button">
13+
<div className="adyen-pe-store-selector-button__labels">
14+
<Typography el={TypographyElement.SPAN} variant={TypographyVariant.BODY} className="adyen-pe-store-selector-button__name">
15+
{name}
16+
</Typography>
17+
<Typography el={TypographyElement.SPAN} variant={TypographyVariant.CAPTION} className="adyen-pe-store-selector-button__description">
18+
{description}
19+
</Typography>
20+
</div>
21+
22+
<Icon name="chevron-up-down" size="medium" className="adyen-pe-store-selector-button__icon" />
23+
</div>
24+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@use '../../../../style';
2+
3+
.adyen-pe-store-selector-item {
4+
display: flex;
5+
flex-direction: column;
6+
7+
&__name {
8+
text-align: left;
9+
}
10+
11+
&__description {
12+
color: style.token(color-label-secondary);
13+
}
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Typography from '../../Typography/Typography';
2+
import { TypographyElement, TypographyVariant } from '../../Typography/types';
3+
import './StoreSelectorItem.scss';
4+
5+
interface StoreSelectorItemProps {
6+
name?: string;
7+
description?: string;
8+
}
9+
10+
export const StoreSelectorItem = ({ name, description }: StoreSelectorItemProps) => (
11+
<div className="adyen-pe-store-selector-item">
12+
<Typography el={TypographyElement.SPAN} variant={TypographyVariant.BODY} className="adyen-pe-store-selector-item__name">
13+
{name}
14+
</Typography>
15+
<Typography el={TypographyElement.SPAN} variant={TypographyVariant.CAPTION} className="adyen-pe-store-selector-item__description">
16+
{description}
17+
</Typography>
18+
</div>
19+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './StoreSelector';

0 commit comments

Comments
 (0)