Skip to content

Commit 9f0eacd

Browse files
[CDX-191] Expose sliderStep prop for FilterRangeSlider and add facet-specific slider steps (#179)
1 parent dcd0c13 commit 9f0eacd

File tree

5 files changed

+242
-6
lines changed

5 files changed

+242
-6
lines changed

spec/components/Filters/Filters.test.jsx

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import CioPlp from '../../../src/components/CioPlp';
66
import Filters from '../../../src/components/Filters';
77
import mockTransformedFacets from '../../local_examples/sampleFacets.json';
88
import testJsonEncodedUrl from '../../local_examples/testJsonEncodedUrl.json';
9-
import { getStateFromUrl } from '../../../src/utils/urlHelpers';
9+
import { getStateFromUrl } from '../../../src/utils';
1010

1111
const filterProps = { facets: mockTransformedFacets };
1212

@@ -212,7 +212,7 @@ describe('Testing Component: Filters', () => {
212212
});
213213
});
214214

215-
it.only('Edge Case: If facet.status.min = 0, should render ranged filters that have already been applied correctly', async () => {
215+
it('Edge Case: If facet.status.min = 0, should render ranged filters that have already been applied correctly', async () => {
216216
const mockPriceFacet = {
217217
displayName: 'Price',
218218
name: 'price',
@@ -260,6 +260,78 @@ describe('Testing Component: Filters', () => {
260260
expect(maxInputSlider.value).toBe(mockPriceFacet.status.max.toString());
261261
});
262262
});
263+
264+
it('Should apply custom sliderStep to range sliders when provided globally', async () => {
265+
const mockPriceFacet = {
266+
displayName: 'Price',
267+
name: 'price',
268+
type: 'range',
269+
data: {},
270+
hidden: false,
271+
min: 1,
272+
max: 100,
273+
status: {},
274+
};
275+
276+
const { container } = render(
277+
<CioPlp apiKey={DEMO_API_KEY}>
278+
<Filters facets={[mockPriceFacet]} sliderStep={0.5} />
279+
</CioPlp>,
280+
);
281+
282+
await waitFor(() => {
283+
const minInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-min-slider');
284+
const maxInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-max-slider');
285+
const minNumberInput = container.querySelector('.cio-slider-input-min input');
286+
const maxNumberInput = container.querySelector('.cio-slider-input-max input');
287+
288+
expect(minInputSlider).toHaveAttribute('step', '0.5');
289+
expect(maxInputSlider).toHaveAttribute('step', '0.5');
290+
expect(minNumberInput).toHaveAttribute('step', '0.5');
291+
expect(maxNumberInput).toHaveAttribute('step', '0.5');
292+
293+
expect(minInputSlider.min).toBe(mockPriceFacet.min.toString());
294+
expect(minInputSlider.max).toBe(mockPriceFacet.max.toString());
295+
expect(maxInputSlider.min).toBe(mockPriceFacet.min.toString());
296+
expect(maxInputSlider.max).toBe(mockPriceFacet.max.toString());
297+
});
298+
});
299+
300+
it('Should apply facet-specific sliderStep when provided', async () => {
301+
const mockPriceFacet = {
302+
displayName: 'Price',
303+
name: 'price',
304+
type: 'range',
305+
data: {},
306+
hidden: false,
307+
min: 1,
308+
max: 100,
309+
status: {},
310+
};
311+
312+
const { container } = render(
313+
<CioPlp apiKey={DEMO_API_KEY}>
314+
<Filters facets={[mockPriceFacet]} sliderStep={0.1} facetSliderSteps={{ price: 1 }} />
315+
</CioPlp>,
316+
);
317+
318+
await waitFor(() => {
319+
const minInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-min-slider');
320+
const maxInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-max-slider');
321+
const minNumberInput = container.querySelector('.cio-slider-input-min input');
322+
const maxNumberInput = container.querySelector('.cio-slider-input-max input');
323+
324+
expect(minInputSlider).toHaveAttribute('step', '1');
325+
expect(maxInputSlider).toHaveAttribute('step', '1');
326+
expect(minNumberInput).toHaveAttribute('step', '1');
327+
expect(maxNumberInput).toHaveAttribute('step', '1');
328+
329+
expect(minInputSlider.min).toBe(mockPriceFacet.min.toString());
330+
expect(minInputSlider.max).toBe(mockPriceFacet.max.toString());
331+
expect(maxInputSlider.min).toBe(mockPriceFacet.min.toString());
332+
expect(maxInputSlider.max).toBe(mockPriceFacet.max.toString());
333+
});
334+
});
263335
});
264336

265337
describe(' - Behavior Tests', () => {
@@ -486,5 +558,119 @@ describe('Testing Component: Filters', () => {
486558
const updatedFiltersWithMaxSliderMove = getRequestFilters(container);
487559
expect(updatedFiltersWithMaxSliderMove.price[0].indexOf('70')).not.toBe(-1);
488560
});
561+
562+
it('SliderRange with custom sliderStep: Should have correct step attribute and behavior', async () => {
563+
function TestFiltersWithCustomStep() {
564+
const mockPriceFacet = {
565+
displayName: 'Price',
566+
name: 'price',
567+
type: 'range',
568+
data: {},
569+
hidden: false,
570+
min: 0,
571+
max: 100,
572+
status: {},
573+
};
574+
575+
const [currentUrl, setCurrentUrl] = useState(testJsonEncodedUrl);
576+
const [filters, setFilters] = useState('');
577+
578+
useEffect(() => {
579+
if (currentUrl !== '') {
580+
const { filters: requestFilters } = getStateFromUrl(currentUrl);
581+
setFilters(JSON.stringify(requestFilters));
582+
}
583+
}, [currentUrl]);
584+
585+
return (
586+
<CioPlp
587+
apiKey={DEMO_API_KEY}
588+
urlHelpers={{ setUrl: setCurrentUrl, getUrl: () => currentUrl }}>
589+
<Filters facets={[mockPriceFacet]} sliderStep={0.5} />
590+
<div id='request-filters'>{filters}</div>
591+
</CioPlp>
592+
);
593+
}
594+
595+
const { container } = render(<TestFiltersWithCustomStep />);
596+
597+
await waitFor(() => {
598+
const minInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-min-slider');
599+
const maxInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-max-slider');
600+
const minNumberInput = container.querySelector('.cio-slider-input-min input');
601+
const maxNumberInput = container.querySelector('.cio-slider-input-max input');
602+
603+
expect(minInputSlider).toHaveAttribute('step', '0.5');
604+
expect(maxInputSlider).toHaveAttribute('step', '0.5');
605+
expect(minNumberInput).toHaveAttribute('step', '0.5');
606+
expect(maxNumberInput).toHaveAttribute('step', '0.5');
607+
608+
fireEvent.change(minNumberInput, { target: { value: 25.5 } });
609+
fireEvent.blur(minNumberInput);
610+
611+
const filters = getRequestFilters(container);
612+
expect(filters.price[0].indexOf('25.5')).not.toBe(-1);
613+
});
614+
});
615+
616+
it('SliderRange with facet-specific sliderStep: Should prioritize facet-specific over global step and handle interactions', async () => {
617+
function TestFiltersWithFacetSpecificStep() {
618+
const mockPriceFacet = {
619+
displayName: 'Price',
620+
name: 'price',
621+
type: 'range',
622+
data: {},
623+
hidden: false,
624+
min: 0,
625+
max: 100,
626+
status: {},
627+
};
628+
629+
const [currentUrl, setCurrentUrl] = useState(testJsonEncodedUrl);
630+
const [filters, setFilters] = useState('');
631+
632+
useEffect(() => {
633+
if (currentUrl !== '') {
634+
const { filters: requestFilters } = getStateFromUrl(currentUrl);
635+
setFilters(JSON.stringify(requestFilters));
636+
}
637+
}, [currentUrl]);
638+
639+
return (
640+
<CioPlp
641+
apiKey={DEMO_API_KEY}
642+
urlHelpers={{ setUrl: setCurrentUrl, getUrl: () => currentUrl }}>
643+
<Filters facets={[mockPriceFacet]} sliderStep={0.1} facetSliderSteps={{ price: 2 }} />
644+
<div id='request-filters'>{filters}</div>
645+
</CioPlp>
646+
);
647+
}
648+
649+
const { container } = render(<TestFiltersWithFacetSpecificStep />);
650+
651+
await waitFor(() => {
652+
const minInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-min-slider');
653+
const maxInputSlider = container.querySelector('.cio-doubly-ended-slider .cio-max-slider');
654+
const minNumberInput = container.querySelector('.cio-slider-input-min input');
655+
const maxNumberInput = container.querySelector('.cio-slider-input-max input');
656+
657+
expect(minInputSlider).toHaveAttribute('step', '2');
658+
expect(maxInputSlider).toHaveAttribute('step', '2');
659+
expect(minNumberInput).toHaveAttribute('step', '2');
660+
expect(maxNumberInput).toHaveAttribute('step', '2');
661+
662+
fireEvent.change(minInputSlider, { target: { value: 24 } });
663+
fireEvent.mouseUp(minInputSlider);
664+
665+
const filtersAfterSliderMove = getRequestFilters(container);
666+
expect(filtersAfterSliderMove.price[0].indexOf('24')).not.toBe(-1);
667+
668+
fireEvent.change(maxNumberInput, { target: { value: 88 } });
669+
fireEvent.blur(maxNumberInput);
670+
671+
const filtersAfterInputChange = getRequestFilters(container);
672+
expect(filtersAfterInputChange.price[0].indexOf('88')).not.toBe(-1);
673+
});
674+
});
489675
});
490676
});

spec/hooks/useFilter/useFilter.test.js

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

88
describe('Testing Hook: useFilter', () => {
@@ -141,4 +141,35 @@ describe('Testing Hook: useFilter', () => {
141141
expect(window.location.href.indexOf('Brand')).toBe(-1);
142142
});
143143
});
144+
145+
it('Should return sliderStep when provided', async () => {
146+
const useFilterPropsWithSliderStep = {
147+
...useFilterProps,
148+
sliderStep: 0.5,
149+
};
150+
const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithSliderStep));
151+
152+
await waitFor(() => {
153+
const {
154+
current: { sliderStep },
155+
} = result;
156+
expect(sliderStep).toBe(0.5);
157+
});
158+
});
159+
160+
it('Should return facetSliderSteps when provided', async () => {
161+
const facetSliderSteps = { price: 1, rating: 0.1 };
162+
const useFilterPropsWithFacetSliderSteps = {
163+
...useFilterProps,
164+
facetSliderSteps,
165+
};
166+
const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithFacetSliderSteps));
167+
168+
await waitFor(() => {
169+
const {
170+
current: { facetSliderSteps: returnedFacetSliderSteps },
171+
} = result;
172+
expect(returnedFacetSliderSteps).toEqual(facetSliderSteps);
173+
});
174+
});
144175
});

src/components/Filters/FilterGroup.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ export interface FilterGroupProps {
99
facet: PlpFacet;
1010
setFilter: UseFilterReturn['setFilter'];
1111
initialNumOptions?: number;
12+
sliderStep?: number;
13+
facetSliderSteps?: Record<string, number>;
1214
}
1315

1416
export default function FilterGroup(props: FilterGroupProps) {
15-
const { facet, setFilter, initialNumOptions = 10 } = props;
17+
const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps } = props;
1618
const [isCollapsed, setIsCollapsed] = useState(false);
1719

1820
const toggleIsCollapsed = () => setIsCollapsed(!isCollapsed);
@@ -41,6 +43,7 @@ export default function FilterGroup(props: FilterGroupProps) {
4143
isCollapsed={isCollapsed}
4244
rangedFacet={facet}
4345
modifyRequestRangeFilter={onFilterSelect(facet.name)}
46+
sliderStep={facetSliderSteps?.[facet.name] || sliderStep}
4447
/>
4548
)}
4649
</li>

src/components/Filters/Filters.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ export type FiltersWithRenderProps = IncludeRenderProps<FiltersProps, UseFilterR
1515

1616
export default function Filters(props: FiltersWithRenderProps) {
1717
const { children, initialNumOptions, ...useFiltersProps } = props;
18-
const { facets, setFilter } = useFilter(useFiltersProps);
18+
const { facets, setFilter, sliderStep, facetSliderSteps } = useFilter(useFiltersProps);
1919

2020
return (
2121
<>
2222
{typeof children === 'function' ? (
2323
children({
2424
facets,
2525
setFilter,
26+
sliderStep,
27+
facetSliderSteps,
2628
})
2729
) : (
2830
<div className='cio-filters'>
@@ -31,6 +33,8 @@ export default function Filters(props: FiltersWithRenderProps) {
3133
facet={facet}
3234
initialNumOptions={initialNumOptions}
3335
setFilter={setFilter}
36+
sliderStep={sliderStep}
37+
facetSliderSteps={facetSliderSteps}
3438
key={facet.name}
3539
/>
3640
))}

src/hooks/useFilter.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,27 @@ import useRequestConfigs from './useRequestConfigs';
55
export interface UseFilterReturn {
66
facets: Array<PlpFacet>;
77
setFilter: (filterName: string, filterValue: PlpFilterValue) => void;
8+
sliderStep?: number;
9+
facetSliderSteps?: Record<string, number>;
810
}
911

1012
export interface UseFilterProps {
1113
/**
1214
* Used to build and render filters dynamically
1315
*/
1416
facets: Array<PlpFacet>;
17+
/**
18+
* Global slider step for all range facets
19+
*/
20+
sliderStep?: number;
21+
/**
22+
* Per-facet slider step configuration
23+
*/
24+
facetSliderSteps?: Record<string, number>;
1525
}
1626

1727
export default function useFilter(props: UseFilterProps): UseFilterReturn {
18-
const { facets } = props;
28+
const { facets, sliderStep, facetSliderSteps } = props;
1929
const contextValue = useCioPlpContext();
2030

2131
if (!contextValue) {
@@ -40,5 +50,7 @@ export default function useFilter(props: UseFilterProps): UseFilterReturn {
4050
return {
4151
facets,
4252
setFilter,
53+
sliderStep,
54+
facetSliderSteps,
4355
};
4456
}

0 commit comments

Comments
 (0)