Skip to content

Commit 88e6431

Browse files
authored
CI-3648 Responsiveness Improvements (#86)
* responsiveness improvements * improving testing coverage * docs improvements * code improvements after review * code improvements
1 parent 0a397b6 commit 88e6431

File tree

15 files changed

+411
-55
lines changed

15 files changed

+411
-55
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { useState } from 'react';
2+
import '@testing-library/jest-dom';
3+
import { render, fireEvent, waitFor } from '@testing-library/react';
4+
import MobileModal from '../../../src/components/MobileModal';
5+
6+
function Wrapper() {
7+
const [isOpen, setIsOpen] = useState(false);
8+
9+
return (
10+
<div>
11+
<button type='button' onClick={() => setIsOpen(true)}>
12+
Open
13+
</button>
14+
<MobileModal isOpen={isOpen} setIsOpen={setIsOpen}>
15+
Content
16+
</MobileModal>
17+
</div>
18+
);
19+
}
20+
21+
describe('MobileModal Component', () => {
22+
it('renders the modal when open', () => {
23+
const { getByText, queryByText } = render(<Wrapper />);
24+
25+
expect(queryByText('Content')).not.toBeInTheDocument();
26+
27+
fireEvent.click(getByText('Open'));
28+
29+
expect(getByText('Content')).toBeInTheDocument();
30+
});
31+
32+
it('should dismiss modal when clicking in backdrop', () => {
33+
const { getByText, queryByText, getByRole } = render(<Wrapper />);
34+
35+
fireEvent.click(getByText('Open'));
36+
37+
expect(getByText('Content')).toBeInTheDocument();
38+
39+
fireEvent.click(getByRole('presentation'));
40+
41+
waitFor(() => {
42+
expect(queryByText('Content')).not.toBeInTheDocument();
43+
});
44+
});
45+
});

spec/components/Pagination/Pagination.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { DEMO_API_KEY } from '../../../src/constants';
1010
const originalWindowLocation = window.location;
1111

1212
beforeEach(() => {
13+
window.innerWidth = 1024;
14+
fireEvent(window, new Event('resize'));
1315
Object.defineProperty(window, 'location', {
1416
value: new URL('https://example.com'),
1517
});
@@ -42,6 +44,24 @@ describe('Pagination Component', () => {
4244
expect(getByText('5')).toBeInTheDocument();
4345
});
4446

47+
it('rerenders the pagination buttons on window resize', () => {
48+
const { getByText, queryByText } = render(
49+
<CioPlp apiKey={DEMO_API_KEY}>
50+
<Pagination totalNumResults={100} />
51+
</CioPlp>,
52+
);
53+
54+
expect(getByText('1')).toBeInTheDocument();
55+
expect(getByText('2')).toBeInTheDocument();
56+
57+
window.innerWidth = 500;
58+
fireEvent(window, new Event('resize'));
59+
60+
waitFor(() => {
61+
expect(queryByText('2')).not.toBeInTheDocument();
62+
});
63+
});
64+
4565
it('renders with render props', () => {
4666
const mockChildren = jest.fn().mockReturnValue(<div>Custom Pagination</div>);
4767
const paginationProps = {

spec/components/Sort/Sort.test.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ describe('Testing Component: Sort', () => {
3535
});
3636

3737
it('Should render sort options based on search or browse response', async () => {
38-
const { getByText } = render(
38+
const { getAllByText } = render(
3939
<CioPlp apiKey={DEMO_API_KEY}>
4040
<Sort sortOptions={searchData.response.sortOptions} />
4141
</CioPlp>,
4242
);
4343

4444
await waitFor(() => {
4545
responseSortOptions.forEach((option) => {
46-
expect(getByText(option.displayName)).toBeInTheDocument();
46+
expect(getAllByText(option.displayName).length).toBeGreaterThanOrEqual(2);
4747
});
4848
});
4949
});
Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,77 @@
11
.cio-plp-grid {
2-
display: grid;
3-
grid-template-columns: 1fr 3fr;
4-
gap: 20px;
5-
padding: 20px;
2+
display: grid;
3+
grid-template-columns: 1fr 3fr;
4+
gap: 20px;
5+
padding: 20px;
66
}
77

88
.cio-filters-container {
9-
padding: 20px;
9+
padding: 20px;
10+
}
11+
12+
.cio-filters-modal-button {
13+
display: flex;
14+
gap: 10px;
15+
align-items: center;
16+
background: none;
17+
border: none;
18+
cursor: pointer;
19+
padding: 10px;
20+
text-align: left;
21+
outline: none;
22+
font-size: 16px;
23+
}
24+
25+
@media (max-width: 768px) {
26+
.cio-plp-grid {
27+
grid-template-columns: 1fr;
28+
}
29+
30+
.cio-plp-grid .cio-product-tiles-container {
31+
grid-template-columns: 1fr 1fr;
32+
}
33+
34+
.cio-products-header-title {
35+
width: 100%;
36+
flex-grow: 1;
37+
text-align: center;
38+
}
1039
}
1140

1241
.cio-products-container {
13-
display: flex;
14-
flex-direction: column;
42+
display: flex;
43+
flex-direction: column;
44+
}
45+
46+
.cio-products-header-wrapper {
47+
justify-content: space-between;
48+
}
49+
50+
.cio-mobile-products-header-wrapper {
51+
justify-content: center;
1552
}
1653

17-
.cio-products-header-container {
18-
display: flex;
19-
flex-direction: row;
20-
justify-content: space-between;
21-
align-items: center;
22-
padding: 10px 5px;
54+
.cio-products-header-wrapper,
55+
.cio-mobile-products-header-wrapper {
56+
display: flex;
57+
flex-direction: row;
58+
align-items: center;
59+
padding: 10px 5px;
2360
}
2461

2562
.cio-product-tiles-container {
26-
display: grid;
27-
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
28-
gap: 10px;
63+
display: grid;
64+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
65+
gap: 10px;
2966
}
3067

3168
.cio-products-container .cio-product-tile {
32-
padding: 5px;
69+
padding: 5px;
3370
}
3471

3572
.cio-products-container .cio-pagination-container {
36-
display: flex;
37-
justify-content: center;
38-
padding: 10px;
39-
margin-top: 20px;
73+
display: flex;
74+
justify-content: center;
75+
padding: 10px;
76+
margin-top: 20px;
4077
}

src/components/CioPlpGrid/CioPlpGrid.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { SearchResponse } from '@constructor-io/constructorio-client-javascript/lib/types';
33
import useSearchResults, { UseSearchResultsReturn } from '../../hooks/useSearchResults';
44
import ProductCard from '../ProductCard';
55
import Filters from '../Filters';
6+
import FiltersIcon from '../Filters/FiltersIcon';
7+
import MobileModal from '../MobileModal';
68
import Sort from '../Sort';
79
import Pagination from '../Pagination';
810
import ZeroResults from './ZeroResults/ZeroResults';
@@ -19,14 +21,28 @@ export type CioPlpGridWithRenderProps = IncludeRenderProps<CioPlpGridProps, UseS
1921

2022
export default function CioPlpGrid(props: CioPlpGridWithRenderProps) {
2123
const { spinner, initialResponse, children } = props;
24+
const [isFilterOpen, setIsFilterOpen] = useState(false);
2225

2326
const { data, status, refetch } = useSearchResults({ initialSearchResponse: initialResponse });
27+
2428
if (isPlpSearchDataRedirect(data)) {
2529
// Do redirect
2630
return null;
2731
}
2832

2933
const response = data?.response;
34+
const searchTerm = data?.request?.term;
35+
36+
const renderTitle = (
37+
<span className='cio-products-header-title'>
38+
<b>{response?.totalNumResults}</b> results
39+
{searchTerm && (
40+
<>
41+
&nbsp;for <b>&quot;{searchTerm}&quot;</b>
42+
</>
43+
)}
44+
</span>
45+
);
3046

3147
return (
3248
<>
@@ -43,17 +59,30 @@ export default function CioPlpGrid(props: CioPlpGridWithRenderProps) {
4359
<>
4460
{response?.results?.length ? (
4561
<div className='cio-plp-grid'>
46-
<div className='cio-filters-container'>
62+
<div className='cio-filters-container cio-large-screen-only'>
4763
<Filters facets={response.facets} />
4864
</div>
4965
<div className='cio-products-container'>
5066
<div className='cio-products-header-container'>
51-
<span>
52-
<b>{response?.totalNumResults}</b> results
53-
</span>
54-
<Sort sortOptions={response.sortOptions} isOpen={false} />
67+
<div className='cio-mobile-products-header-wrapper cio-mobile-only'>
68+
{renderTitle}
69+
</div>
70+
<div className='cio-products-header-wrapper'>
71+
<button
72+
type='button'
73+
className='cio-filters-modal-button cio-mobile-only'
74+
onClick={() => setIsFilterOpen(!isFilterOpen)}>
75+
{FiltersIcon}
76+
Filters
77+
</button>
78+
<span className='cio-large-screen-only'>{renderTitle}</span>
79+
<Sort sortOptions={response.sortOptions} isOpen={false} />
80+
</div>
5581
</div>
5682
<div className='cio-product-tiles-container'>
83+
<MobileModal isOpen={isFilterOpen} setIsOpen={setIsFilterOpen}>
84+
<Filters facets={response.facets} />
85+
</MobileModal>
5786
{response?.results?.map((item) => (
5887
<div className='cio-product-tile'>
5988
<ProductCard key={item.itemId} item={item} />

src/components/Filters/FilterOptionsList.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@
5858

5959
.cio-filter-option-name {
6060
flex-grow: 1;
61+
word-break: break-word;
6162
}
6263

6364
.cio-filter-option-count {
6465
color: #999;
66+
margin-left: 8px;
6567
}

src/components/Filters/FilterRangeSlider.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
padding: 2px;
1818
}
1919

20+
@media (max-width: 320px) {
21+
.cio-slider-inputs {
22+
row-gap: 20px;
23+
grid-template-columns: 1fr;
24+
}
25+
}
26+
2027
.cio-slider-input {
2128
position: relative;
2229
display: flex;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
export default (
4+
<svg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'>
5+
<path
6+
d='M0.850098 5H3.5M19.1501 5H9.5'
7+
stroke='#999999'
8+
strokeWidth='1.7'
9+
strokeLinecap='round'
10+
strokeLinejoin='round'
11+
/>
12+
<path
13+
d='M9.5 5C9.5 6.65685 8.15685 8 6.5 8C4.84315 8 3.5 6.65685 3.5 5C3.5 3.34315 4.84315 2 6.5 2C8.15685 2 9.5 3.34315 9.5 5Z'
14+
stroke='#999999'
15+
strokeWidth='1.7'
16+
strokeLinecap='round'
17+
strokeLinejoin='round'
18+
/>
19+
<path
20+
d='M19.1499 15H16.5M0.849903 15H10.5'
21+
stroke='#999999'
22+
strokeWidth='1.7'
23+
strokeLinecap='round'
24+
strokeLinejoin='round'
25+
/>
26+
<path
27+
d='M10.5 15C10.5 16.6569 11.8431 18 13.5 18C15.1569 18 16.5 16.6569 16.5 15C16.5 13.3431 15.1569 12 13.5 12C11.8431 12 10.5 13.3431 10.5 15Z'
28+
stroke='#999999'
29+
strokeWidth='1.7'
30+
strokeLinecap='round'
31+
strokeLinejoin='round'
32+
/>
33+
</svg>
34+
);

0 commit comments

Comments
 (0)