Skip to content

Commit 1d6ad47

Browse files
HHHindawyHossam HindawyMudaafi
authored
[CSL-3157] Sort Component (#62)
* csl-3157 Sort Component * Refactor * Update styles --------- Co-authored-by: Hossam Hindawy <hossam.hindawy@hossam.hindawy-Q7M9F6QK6G> Co-authored-by: Ahmad Mudaafi <ahmad.mudaafi@constructor.io>
1 parent 09bdf31 commit 1d6ad47

File tree

14 files changed

+451
-58
lines changed

14 files changed

+451
-58
lines changed

spec/Sort/Sort.server.test.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { renderToString } from 'react-dom/server';
3+
import Sort from '../../src/components/Sort/Sort';
4+
import CioPlp from '../../src/components/CioPlp';
5+
import { DEMO_API_KEY } from '../../src/constants';
6+
import '@testing-library/jest-dom';
7+
import { transformSearchResponse } from '../../src/utils/transformers';
8+
import mockSearchResponse from '../local_examples/apiSearchResponse.json';
9+
10+
describe('Testing Component on the server: Sort', () => {
11+
beforeEach(() => {
12+
// Mock console error to de-clutter the console for expected errors
13+
const spy = jest.spyOn(console, 'error');
14+
spy.mockImplementation(() => {});
15+
});
16+
17+
afterAll(() => {
18+
jest.resetAllMocks(); // This will reset all mocks after each test
19+
});
20+
21+
const searchResponse = transformSearchResponse(mockSearchResponse);
22+
const responseSortOptions = searchResponse.sortOptions;
23+
24+
it('Should throw error if used outside the CioPlp', () => {
25+
expect(() => renderToString(<Sort searchOrBrowseResponse={searchResponse} />)).toThrow();
26+
});
27+
28+
it('Should render sort options based on search or browse response', async () => {
29+
const html = renderToString(
30+
<CioPlp apiKey={DEMO_API_KEY}>
31+
<Sort searchOrBrowseResponse={searchResponse} />
32+
</CioPlp>,
33+
);
34+
35+
responseSortOptions.forEach((option) => {
36+
expect(html).toContain(option.displayName);
37+
});
38+
});
39+
40+
it('Should render correctly with render props', () => {
41+
const mockChildren = jest.fn().mockReturnValue(<div>Custom Sort</div>);
42+
43+
const sortProps = {
44+
searchOrBrowseResponse: searchResponse,
45+
children: mockChildren,
46+
};
47+
48+
const html = renderToString(
49+
<CioPlp apiKey={DEMO_API_KEY}>
50+
<Sort {...sortProps} />
51+
</CioPlp>,
52+
);
53+
expect(mockChildren).toHaveBeenCalled();
54+
expect(html).toContain('Custom Sort');
55+
});
56+
});

spec/Sort/Sort.test.jsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from 'react';
2+
import { render, waitFor, fireEvent } from '@testing-library/react';
3+
import Sort from '../../src/components/Sort/Sort';
4+
import CioPlp from '../../src/components/CioPlp';
5+
import { DEMO_API_KEY } from '../../src/constants';
6+
import '@testing-library/jest-dom';
7+
import { transformSearchResponse } from '../../src/utils/transformers';
8+
import mockSearchResponse from '../local_examples/apiSearchResponse.json';
9+
10+
describe('Testing Component: Sort', () => {
11+
let location;
12+
const mockLocation = new URL('https://example.com');
13+
14+
beforeEach(() => {
15+
// Mock console error to de-clutter the console for expected errors
16+
const spy = jest.spyOn(console, 'error');
17+
spy.mockImplementation(() => {});
18+
19+
location = window.location;
20+
delete window.location;
21+
window.location = mockLocation;
22+
mockLocation.href = 'https://example.com/';
23+
});
24+
25+
afterAll(() => {
26+
window.location = location;
27+
jest.resetAllMocks(); // This will reset all mocks after each test
28+
});
29+
30+
const searchResponse = transformSearchResponse(mockSearchResponse);
31+
const responseSortOptions = searchResponse.sortOptions;
32+
33+
it('Should throw error if used outside the CioPlp', () => {
34+
expect(() => render(<Sort searchOrBrowseResponse={searchResponse} />)).toThrow();
35+
});
36+
37+
it('Should render sort options based on search or browse response', async () => {
38+
const { getByText } = render(
39+
<CioPlp apiKey={DEMO_API_KEY}>
40+
<Sort searchOrBrowseResponse={searchResponse} />
41+
</CioPlp>,
42+
);
43+
44+
await waitFor(() => {
45+
responseSortOptions.forEach((option) => {
46+
expect(getByText(option.displayName)).toBeInTheDocument();
47+
});
48+
});
49+
});
50+
51+
it('Should change selected sort option in component correctly', async () => {
52+
const { container } = render(
53+
<CioPlp apiKey={DEMO_API_KEY}>
54+
<Sort searchOrBrowseResponse={searchResponse} />
55+
</CioPlp>,
56+
);
57+
58+
const defaultSort = responseSortOptions.find((option) => option.status === 'selected');
59+
expect(container.querySelector('input:checked')).toBeDefined();
60+
expect(container.querySelector('input:checked').value).toBe(JSON.stringify(defaultSort));
61+
62+
const newSortOption = responseSortOptions.find((option) => option.status !== 'selected');
63+
fireEvent.click(
64+
container?.querySelector(`#${newSortOption.sortBy}-${newSortOption.sortOrder}`),
65+
);
66+
67+
expect(container.querySelector('input:checked')).toBeDefined();
68+
expect(container.querySelector('input:checked').value).toBe(JSON.stringify(newSortOption));
69+
});
70+
71+
it('Should change selected sort option in url correctly', async () => {
72+
const { container } = render(
73+
<CioPlp apiKey={DEMO_API_KEY}>
74+
<Sort searchOrBrowseResponse={searchResponse} />
75+
</CioPlp>,
76+
);
77+
78+
const newSortOption = responseSortOptions.find((option) => option.status !== 'selected');
79+
fireEvent.click(
80+
container?.querySelector(`#${newSortOption.sortBy}-${newSortOption.sortOrder}`),
81+
);
82+
83+
const urlObject = new URL(window.location.href);
84+
expect(urlObject.searchParams.get('sortBy')).toEqual(newSortOption.sortBy);
85+
expect(urlObject.searchParams.get('sortOrder')).toEqual(newSortOption.sortOrder);
86+
});
87+
88+
it('Should render correctly with render props', () => {
89+
const mockChildren = jest.fn().mockReturnValue(<div>Custom Sort</div>);
90+
91+
const sortProps = {
92+
searchOrBrowseResponse: searchResponse,
93+
children: mockChildren,
94+
};
95+
96+
const { getByText } = render(
97+
<CioPlp apiKey={DEMO_API_KEY}>
98+
<Sort {...sortProps} />
99+
</CioPlp>,
100+
);
101+
expect(mockChildren).toHaveBeenCalled();
102+
expect(getByText('Custom Sort')).toBeInTheDocument();
103+
});
104+
});

spec/useSort.server.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import '@testing-library/jest-dom';
22
import useSort from '../src/hooks/useSort';
3-
import { transformSearchResponse, transformSortOptionsResponse } from '../src/utils/transformers';
3+
import { transformSearchResponse } from '../src/utils/transformers';
44
import mockSearchResponse from './local_examples/apiSearchResponse.json';
55
import { renderHookServerSide, renderHookServerSideWithCioPlp } from './test-utils.server';
66
import { DEMO_API_KEY } from '../src/constants';
@@ -17,7 +17,7 @@ describe('Testing Hook on the server: useSort', () => {
1717
});
1818

1919
const searchResponse = transformSearchResponse(mockSearchResponse);
20-
const responseSortOptions = transformSortOptionsResponse(searchResponse.sortOptions);
20+
const responseSortOptions = searchResponse.sortOptions;
2121

2222
it('Should throw an error if called outside of PlpContext', () => {
2323
expect(() => renderHookServerSide(() => useSort(searchResponse))).toThrow();

spec/useSort.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import '@testing-library/jest-dom';
22
import { renderHook, waitFor } from '@testing-library/react';
33
import useSort from '../src/hooks/useSort';
4-
import { transformSearchResponse, transformSortOptionsResponse } from '../src/utils/transformers';
4+
import { transformSearchResponse } from '../src/utils/transformers';
55
import mockSearchResponse from './local_examples/apiSearchResponse.json';
66
import { renderHookWithCioPlp } from './test-utils';
77

@@ -26,7 +26,7 @@ describe('Testing Hook: useSort', () => {
2626
});
2727

2828
const searchResponse = transformSearchResponse(mockSearchResponse);
29-
const responseSortOptions = transformSortOptionsResponse(searchResponse.sortOptions);
29+
const responseSortOptions = searchResponse.sortOptions;
3030

3131
it('Should throw error if called outside of PlpContext', () => {
3232
expect(() => renderHook(() => useSort())).toThrow();

src/components/Sort/Sort.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useState } from 'react';
2+
import useSort from '../../hooks/useSort';
3+
import {
4+
IncludeRenderProps,
5+
PlpBrowseResponse,
6+
PlpSearchResponse,
7+
UseSortReturn,
8+
} from '../../types';
9+
10+
type SortProps = {
11+
/**
12+
* Default open state of dropdown
13+
*/
14+
isOpen?: boolean;
15+
/**
16+
* Used to build and render sort options dynamically
17+
*/
18+
searchOrBrowseResponse: PlpBrowseResponse | PlpSearchResponse;
19+
};
20+
type SortWithRenderProps = IncludeRenderProps<SortProps, UseSortReturn>;
21+
22+
export default function Sort({
23+
isOpen: defaultOpen = true,
24+
searchOrBrowseResponse,
25+
children,
26+
}: SortWithRenderProps) {
27+
const [isOpen, setIsOpen] = useState(defaultOpen);
28+
const { sortOptions, selectedSort, changeSelectedSort } = useSort(searchOrBrowseResponse);
29+
30+
const toggleCollapsible = () => {
31+
setIsOpen(!isOpen);
32+
};
33+
34+
const handleOptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
35+
changeSelectedSort(JSON.parse(event.target.value));
36+
};
37+
38+
return (
39+
<>
40+
{typeof children === 'function' ? (
41+
children({
42+
sortOptions,
43+
selectedSort,
44+
changeSelectedSort,
45+
})
46+
) : (
47+
<div className='cio-plp-sort'>
48+
<button type='button' className='collapsible' onClick={toggleCollapsible}>
49+
Sort
50+
<i className={`arrow ${isOpen ? 'arrow-up' : 'arrow-down'}`} />
51+
</button>
52+
{isOpen && (
53+
<div className='collapsible-content'>
54+
{sortOptions.map((option) => (
55+
<label
56+
htmlFor={`${option.sortBy}-${option.sortOrder}`}
57+
key={`${option.sortBy}-${option.sortOrder}`}>
58+
<input
59+
id={`${option.sortBy}-${option.sortOrder}`}
60+
type='radio'
61+
name={`${option.sortBy}-${option.sortOrder}`}
62+
value={JSON.stringify(option)}
63+
checked={
64+
selectedSort?.sortBy === option.sortBy &&
65+
selectedSort.sortOrder === option.sortOrder
66+
}
67+
onChange={handleOptionChange}
68+
/>
69+
<span>{option.displayName}</span>
70+
</label>
71+
))}
72+
</div>
73+
)}
74+
</div>
75+
)}
76+
</>
77+
);
78+
}
79+
80+
Sort.defaultProps = {
81+
isOpen: true,
82+
};

src/components/Sort/sort.css

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
.cio-plp-sort {
2+
background-color: #FFFFFF;
3+
width: 100%;
4+
box-sizing: border-box;
5+
padding: 10px;
6+
}
7+
8+
.cio-plp-sort .collapsible {
9+
background-color: #FFFFFF;
10+
border: none;
11+
border-bottom: 1px solid #0000001A;
12+
cursor: pointer;
13+
padding: 10px;
14+
width: 100%;
15+
min-width: 180px;
16+
text-align: left;
17+
outline: none;
18+
font-size: 18px;
19+
display: flex;
20+
justify-content: space-between;
21+
align-items: center;
22+
}
23+
24+
.cio-plp-sort .collapsible-content {
25+
padding: 10px;
26+
background-color: #FFFFFF;
27+
display: flex;
28+
flex-wrap: wrap;
29+
flex-direction: column;
30+
}
31+
32+
.cio-plp-sort label {
33+
cursor: pointer;
34+
overflow: hidden;
35+
width: 100%;
36+
}
37+
38+
.cio-plp-sort input {
39+
display: none;
40+
}
41+
42+
.cio-plp-sort label span {
43+
display: flex;
44+
gap: 10px;
45+
align-items: center;
46+
padding: 10px;
47+
transition: 0.25s ease;
48+
}
49+
50+
/* Apply changes with a smooth and gradual transition */
51+
.cio-plp-sort label span:before {
52+
display: flex;
53+
flex-shrink: 0;
54+
content: "";
55+
background-color: #FFFFFF;
56+
width: 20px;
57+
height: 20px;
58+
border-radius: 50%;
59+
transition: 0.25s ease;
60+
border: 1px solid #00000033;
61+
}
62+
63+
.cio-plp-sort input:checked+span:before {
64+
box-shadow: inset 0 0 0 6px #000000;
65+
}
66+
67+
.cio-plp-sort .arrow {
68+
border: 1px solid #00000099;
69+
border-width: 0 2px 2px 0;
70+
display: inline-block;
71+
padding: 4px;
72+
}
73+
74+
.cio-plp-sort .arrow-up {
75+
transform: rotate(-135deg);
76+
-webkit-transform: rotate(-135deg);
77+
margin-bottom: -6px;
78+
}
79+
80+
.cio-plp-sort .arrow-down {
81+
transform: rotate(45deg);
82+
-webkit-transform: rotate(45deg);
83+
margin-top: -6px;
84+
}

src/hooks/useSort.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { useCioPlpContext } from './useCioPlpContext';
33
import { PlpBrowseResponse, PlpSearchResponse, PlpSortOption, UseSortReturn } from '../types';
4-
import { transformSortOptionsResponse } from '../utils/transformers';
54
import useRequestConfigs from './useRequestConfigs';
65

76
const useSort = (searchOrBrowseResponse: PlpBrowseResponse | PlpSearchResponse): UseSortReturn => {
@@ -13,7 +12,7 @@ const useSort = (searchOrBrowseResponse: PlpBrowseResponse | PlpSearchResponse):
1312

1413
const [selectedSort, setSelectedSort] = useState<PlpSortOption | null>(null);
1514

16-
const sortOptions = transformSortOptionsResponse(searchOrBrowseResponse.sortOptions);
15+
const { sortOptions } = searchOrBrowseResponse;
1716
const {
1817
requestConfigs: { sortBy, sortOrder },
1918
setRequestConfigs,

0 commit comments

Comments
 (0)