From c2614bdadef23725a7d93d977025747c15ad385a Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:47:30 +0100 Subject: [PATCH] refactor(range): make range connector more consistent and simpler to use (#6508) BREAKING CHANGE: `start` is now called `currentRefinement` and its data structure and behavior are different --- .../e-commerce/components/PriceSlider.tsx | 40 ++-- .../connectors/__tests__/connectRange.test.ts | 174 +++++++++++++----- .../src/connectors/connectRange.ts | 48 ++--- .../src/components/RangeInput/RangeInput.tsx | 12 +- .../RangeInput/__tests__/RangeInput-test.tsx | 13 +- .../src/components/Slider/Slider.tsx | 19 +- .../Slider/__tests__/Slider-test.tsx | 10 +- .../instantsearch.js/src/connectors/index.ts | 1 - .../src/widgets/range-input/range-input.tsx | 12 +- .../__snapshots__/range-slider-test.ts.snap | 48 ++--- .../__tests__/range-slider-test.ts | 50 +---- .../src/widgets/range-slider/range-slider.tsx | 21 +-- .../connectors/__tests__/useRange.test.tsx | 4 +- .../react-instantsearch/src/ui/RangeInput.tsx | 36 ++-- .../src/ui/__tests__/RangeInput.test.tsx | 29 +-- .../src/widgets/RangeInput.tsx | 11 +- .../src/components/RangeInput.vue | 25 +-- .../src/components/__tests__/RangeInput.js | 2 +- 18 files changed, 280 insertions(+), 275 deletions(-) diff --git a/examples/react/e-commerce/components/PriceSlider.tsx b/examples/react/e-commerce/components/PriceSlider.tsx index 0d9e9e9c65..4d0da7f64d 100644 --- a/examples/react/e-commerce/components/PriceSlider.tsx +++ b/examples/react/e-commerce/components/PriceSlider.tsx @@ -1,4 +1,4 @@ -import { Range, RangeBoundaries } from 'instantsearch-core'; +import { Range } from 'instantsearch-core'; import React, { useState } from 'react'; import { Slider, @@ -56,16 +56,13 @@ function Handle({ ); } -function convertToTicks(start: RangeBoundaries, range: Range): number[] { +function convertToTicks({ min, max }: Range, range: Range): readonly number[] { const domain = range.min === 0 && range.max === 0 ? { min: undefined, max: undefined } : range; - return [ - start[0] === -Infinity ? domain.min! : start[0]!, - start[1] === Infinity ? domain.max! : start[1]!, - ]; + return [min || domain.min!, max || domain.max!]; } export function PriceSlider({ @@ -77,7 +74,7 @@ export function PriceSlider({ min?: number; max?: number; }) { - const { range, start, refine, canRefine } = useRange( + const { range, currentRefinement, refine, canRefine } = useRange( { attribute, min, @@ -85,22 +82,16 @@ export function PriceSlider({ }, { $$widgetType: 'e-commerce.rangeSlider' } ); - const [ticksValues, setTicksValues] = useState(convertToTicks(start, range)); - const [prevStart, setPrevStart] = useState(start); + const [ticksValues, setTicksValues] = useState( + convertToTicks(currentRefinement, range) + ); + const [prevRefinement, setPrevRefinement] = useState(currentRefinement); - if (start !== prevStart) { - setTicksValues(convertToTicks(start, range)); - setPrevStart(start); + if (currentRefinement !== prevRefinement) { + setTicksValues(convertToTicks(currentRefinement, range)); + setPrevRefinement(currentRefinement); } - const onChange = (values: readonly number[]) => { - refine(values as [number, number]); - }; - - const onUpdate = (values: readonly number[]) => { - setTicksValues(values as [number, number]); - }; - if ( !canRefine || ticksValues[0] === undefined || @@ -114,10 +105,13 @@ export function PriceSlider({ mode={2} step={1} domain={[range.min!, range.max!]} - values={start as number[]} + values={[ + currentRefinement.min || range.min!, + currentRefinement.max || range.max!, + ]} disabled={!canRefine} - onChange={onChange} - onUpdate={onUpdate} + onChange={([newMin, newMax]) => refine({ min: newMin, max: newMax })} + onUpdate={(values) => setTicksValues(values)} rootStyle={{ position: 'relative', marginTop: '1.5rem' }} className="ais-RangeSlider" > diff --git a/packages/instantsearch-core/src/connectors/__tests__/connectRange.test.ts b/packages/instantsearch-core/src/connectors/__tests__/connectRange.test.ts index ec8701b910..fafd8ae220 100644 --- a/packages/instantsearch-core/src/connectors/__tests__/connectRange.test.ts +++ b/packages/instantsearch-core/src/connectors/__tests__/connectRange.test.ts @@ -132,10 +132,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input expect(isFirstRendering).toBe(true); // should provide good values for the first rendering - const { range, start, widgetParams } = + const { range, currentRefinement, widgetParams } = rendering.mock.calls[rendering.mock.calls.length - 1][0]; expect(range).toEqual({ min: 0, max: 0 }); - expect(start).toEqual([-Infinity, Infinity]); + expect(currentRefinement).toEqual({ min: undefined, max: undefined }); expect(widgetParams).toEqual({ attribute, precision: 0, @@ -162,10 +162,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input expect(isFirstRendering).toBe(false); // should provide good values for the first rendering - const { range, start, widgetParams } = + const { range, currentRefinement, widgetParams } = rendering.mock.calls[rendering.mock.calls.length - 1][0]; expect(range).toEqual({ min: 10, max: 30 }); - expect(start).toEqual([-Infinity, Infinity]); + expect(currentRefinement).toEqual({ min: undefined, max: undefined }); expect(widgetParams).toEqual({ attribute, precision: 0, @@ -255,7 +255,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; - refine([10, 30]); + refine({ min: 10, max: 30 }); expect(helper.getNumericRefinement('price', '>=')).toEqual([10]); expect(helper.getNumericRefinement('price', '<=')).toEqual([30]); expect(helper.search).toHaveBeenCalledTimes(1); @@ -281,7 +281,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; - refine([23, 27]); + refine({ min: 23, max: 27 }); expect(helper.getNumericRefinement('price', '>=')).toEqual([23]); expect(helper.getNumericRefinement('price', '<=')).toEqual([27]); expect(helper.search).toHaveBeenCalledTimes(2); @@ -312,13 +312,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; - refine([10, 30]); + refine({ min: 10, max: 30 }); expect(helper.getNumericRefinement('price', '>=')).toEqual([10]); expect(helper.getNumericRefinement('price', '<=')).toEqual([30]); expect(helper.search).toHaveBeenCalledTimes(1); - refine([0, undefined]); + refine({ min: 0, max: undefined }); expect(helper.getNumericRefinement('price', '>=')).toEqual([0]); expect(helper.getNumericRefinement('price', '<=')).toEqual([500]); } @@ -356,13 +356,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; - refine([10, 30]); + refine({ min: 10, max: 30 }); expect(helper.getNumericRefinement('price', '>=')).toEqual([10]); expect(helper.getNumericRefinement('price', '<=')).toEqual([30]); expect(helper.search).toHaveBeenCalledTimes(1); - refine([0, undefined]); + refine({ min: 0, max: undefined }); expect(helper.getNumericRefinement('price', '>=')).toEqual([0]); expect(helper.getNumericRefinement('price', '<=')).toEqual([500]); } @@ -396,17 +396,17 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; - refine([undefined, 100]); + refine({ min: undefined, max: 100 }); expect(helper.getNumericRefinement('price', '>=')).toEqual(undefined); expect(helper.getNumericRefinement('price', '<=')).toEqual([100]); expect(helper.search).toHaveBeenCalledTimes(1); - refine([0, undefined]); + refine({ min: 0, max: undefined }); expect(helper.getNumericRefinement('price', '>=')).toEqual([0]); expect(helper.getNumericRefinement('price', '<=')).toEqual([]); expect(helper.search).toHaveBeenCalledTimes(2); - refine([0, 100]); + refine({ min: 0, max: 100 }); expect(helper.getNumericRefinement('price', '>=')).toEqual([0]); expect(helper.getNumericRefinement('price', '<=')).toEqual([100]); expect(helper.search).toHaveBeenCalledTimes(3); @@ -580,7 +580,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }); }); - describe('start', () => { + describe('currentRefinement', () => { const attribute = 'price'; const rendering = () => {}; const createHelper = () => jsHelper(createSearchClient(), ''); @@ -593,7 +593,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input widget.getWidgetSearchParameters(helper.state, { uiState: {} }) ); - const { start } = widget.getWidgetRenderState( + const { currentRefinement } = widget.getWidgetRenderState( createRenderOptions({ results: createFacetStatsResults({ helper, @@ -604,7 +604,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - expect(start).toEqual([-Infinity, Infinity]); + expect(currentRefinement).toEqual({ min: undefined, max: undefined }); }); it('expect to return refinement from helper', () => { @@ -617,7 +617,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input helper.addNumericRefinement(attribute, '>=', 10); helper.addNumericRefinement(attribute, '<=', 100); - const { start } = widget.getWidgetRenderState( + const { currentRefinement } = widget.getWidgetRenderState( createRenderOptions({ helper, results: createFacetStatsResults({ @@ -629,7 +629,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - expect(start).toEqual([10, 100]); + expect(currentRefinement).toEqual({ min: 10, max: 100 }); }); it('expect to return float refinement values', () => { @@ -642,7 +642,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input helper.addNumericRefinement(attribute, '>=', 10.9); helper.addNumericRefinement(attribute, '<=', 99.1); - const { start } = widget.getWidgetRenderState( + const { currentRefinement } = widget.getWidgetRenderState( createRenderOptions({ helper, results: createFacetStatsResults({ @@ -654,7 +654,85 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - expect(start).toEqual([10.9, 99.1]); + expect(currentRefinement).toEqual({ min: 10.9, max: 99.1 }); + }); + + it('expect to return undefined values when refinement is equal to range', () => { + const widget = connectRange(rendering)({ attribute }); + const helper = createHelper(); + + helper.setState( + widget.getWidgetSearchParameters(helper.state, { uiState: {} }) + ); + helper.addNumericRefinement(attribute, '>=', 1); + helper.addNumericRefinement(attribute, '<=', 4999); + + const { currentRefinement } = widget.getWidgetRenderState( + createRenderOptions({ + helper, + results: createFacetStatsResults({ + helper, + attribute, + min: 1, + max: 4999, + }), + }) + ); + + expect(currentRefinement).toEqual({ + min: undefined, + max: undefined, + }); + }); + + it('expect to clamp min refinement value to max range', () => { + const widget = connectRange(rendering)({ attribute }); + const helper = createHelper(); + + helper.setState( + widget.getWidgetSearchParameters(helper.state, { uiState: {} }) + ); + helper.addNumericRefinement(attribute, '>=', 5000); + helper.addNumericRefinement(attribute, '<=', 6000); + + const { currentRefinement } = widget.getWidgetRenderState( + createRenderOptions({ + helper, + results: createFacetStatsResults({ + helper, + attribute, + min: 1, + max: 4999, + }), + }) + ); + + expect(currentRefinement.min).toBe(4999); + }); + + it('expect to clamp max refinement value to min range', () => { + const widget = connectRange(rendering)({ attribute }); + const helper = createHelper(); + + helper.setState( + widget.getWidgetSearchParameters(helper.state, { uiState: {} }) + ); + helper.addNumericRefinement(attribute, '>=', -50); + helper.addNumericRefinement(attribute, '<=', 0); + + const { currentRefinement } = widget.getWidgetRenderState( + createRenderOptions({ + helper, + results: createFacetStatsResults({ + helper, + attribute, + min: 1, + max: 4999, + }), + }) + ); + + expect(currentRefinement.max).toBe(1); }); }); @@ -679,7 +757,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input createInitOptions({ helper }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.state.page).toBe(0); }); @@ -694,7 +772,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input createInitOptions({ helper }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -724,7 +802,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -755,7 +833,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input ); // @ts-expect-error - refine(['10', '490']); + refine({ min: '10', max: '490' }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -784,7 +862,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input ); // @ts-expect-error - refine(['10.50', '490.50']); + refine({ min: '10.50', max: '490.50' }); // min is rounded down, max rounded up expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); @@ -813,7 +891,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -844,7 +922,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -880,7 +958,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([undefined, 490]); + refine({ min: undefined, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -913,7 +991,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, undefined]); + refine({ min: 10, max: undefined }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([]); @@ -947,7 +1025,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input ); // @ts-expect-error - refine(['', 490]); + refine({ min: '', max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -981,7 +1059,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input ); // @ts-expect-error - refine([10, '']); + refine({ min: 10, max: '' }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([]); @@ -1013,7 +1091,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([0, 490]); + refine({ min: 0, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -1045,7 +1123,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 500]); + refine({ min: 10, max: 500 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([]); @@ -1080,7 +1158,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([undefined, 490]); + refine({ min: undefined, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -1113,7 +1191,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, undefined]); + refine({ min: 10, max: undefined }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([250]); @@ -1141,7 +1219,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([0, 490]); + refine({ min: 0, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined); expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined); @@ -1169,7 +1247,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 500]); + refine({ min: 10, max: 500 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined); expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined); @@ -1197,7 +1275,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([undefined, undefined]); + refine({ min: undefined, max: undefined }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined); expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined); @@ -1228,7 +1306,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input }) ); - refine([10, 490]); + refine({ min: 10, max: 490 }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]); expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]); @@ -1256,7 +1334,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input ); // @ts-expect-error - refine(['ADASA', 'FFDSFQS']); + refine({ min: 'ADASA', max: 'FFDSFQS' }); expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined); expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined); @@ -1328,7 +1406,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input const renderOptions = rendering.mock.calls[0][0]; const { refine } = renderOptions; - refine([100, 1000]); + refine({ min: 100, max: 1000 }); expect(helper.state).toEqual( new SearchParameters({ @@ -1624,7 +1702,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input canRefine: false, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [0, 1000], + currentRefinement: { min: undefined, max: 1000 }, widgetParams: { attribute: 'price', precision: 0, @@ -1659,7 +1737,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input canRefine: true, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [0, 1000], + currentRefinement: { min: 0, max: 1000 }, widgetParams: { attribute: 'price', precision: 0, @@ -1703,7 +1781,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input canRefine: false, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [0, 1000], + currentRefinement: { min: undefined, max: 1000 }, widgetParams: { attribute: 'price', precision: 0, @@ -1735,7 +1813,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input canRefine: true, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [0, 1000], + currentRefinement: { min: 0, max: 1000 }, widgetParams: { attribute: 'price', precision: 0, @@ -1779,7 +1857,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input canRefine: false, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [-Infinity, Infinity], + currentRefinement: { min: undefined, max: undefined }, widgetParams: { attribute: 'price', precision: 0, @@ -2210,7 +2288,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-input expect(renderer).toHaveBeenCalledWith( expect.objectContaining({ - start: [100, 200], + currentRefinement: { min: 100, max: 200 }, }), true ); @@ -2246,7 +2324,7 @@ describe('insights', () => { const firstRenderingOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = firstRenderingOptions; - refine([10, 30]); + refine({ min: 10, max: 30 }); expect(instantSearchInstance.sendEventToInsights).not.toHaveBeenCalled(); }); diff --git a/packages/instantsearch-core/src/connectors/connectRange.ts b/packages/instantsearch-core/src/connectors/connectRange.ts index 314108070f..e6001724b5 100644 --- a/packages/instantsearch-core/src/connectors/connectRange.ts +++ b/packages/instantsearch-core/src/connectors/connectRange.ts @@ -2,7 +2,12 @@ import { createDocumentationMessageGenerator, noop } from '../lib/public'; import { checkRendering } from '../lib/utils'; import type { InstantSearch } from '../instantsearch'; -import type { Connector, WidgetRenderState, SendEventForFacet } from '../types'; +import type { + Connector, + Expand, + WidgetRenderState, + SendEventForFacet, +} from '../types'; import type { AlgoliaSearchHelper, SearchResults } from 'algoliasearch-helper'; const withUsage = createDocumentationMessageGenerator( @@ -15,8 +20,6 @@ const $$type = 'ais.range'; export type RangeMin = number | undefined; export type RangeMax = number | undefined; -// @MAJOR: potentially we should consolidate these types -export type RangeBoundaries = [RangeMin, RangeMax]; export type Range = { min: RangeMin; max: RangeMax; @@ -25,11 +28,10 @@ export type Range = { export type RangeRenderState = { /** * Sets a range to filter the results on. Both values - * are optional, and will default to the higher and lower bounds. You can use `undefined` to remove a - * previously set bound or to set an infinite bound. - * @param rangeValue tuple of [min, max] bounds + * are optional, and will default to the higher and lower bounds. You can use `undefined` to remove a previously set bound. + * @param rangeValue object with min and max bounds */ - refine: (rangeValue: RangeBoundaries) => void; + refine: (rangeValue: Expand>) => void; /** * Indicates whether this widget can be refined @@ -49,7 +51,7 @@ export type RangeRenderState = { /** * Current refinement of the search */ - start: RangeBoundaries; + currentRefinement: Range; /** * Transform for the rendering `from` and/or `to` values. @@ -96,7 +98,6 @@ export type RangeWidgetDescription = { }; indexUiState: { range: { - // @TODO: this could possibly become `${number}:${number}` later [attribute: string]: string; }; }; @@ -284,26 +285,31 @@ export const connectRange: RangeConnector = function connectRange( } function _getCurrentRefinement( - helper: AlgoliaSearchHelper - ): RangeBoundaries { + helper: AlgoliaSearchHelper, + range: Range + ): Range { const [minValue] = helper.getNumericRefinement(attribute, '>=') || []; const [maxValue] = helper.getNumericRefinement(attribute, '<=') || []; const min = - typeof minValue === 'number' && Number.isFinite(minValue) - ? minValue - : -Infinity; + typeof minValue === 'number' && + Number.isFinite(minValue) && + minValue !== range.min + ? Math.min(minValue, range.max!) + : undefined; const max = - typeof maxValue === 'number' && Number.isFinite(maxValue) - ? maxValue - : Infinity; + typeof maxValue === 'number' && + Number.isFinite(maxValue) && + maxValue !== range.max + ? Math.max(maxValue, range.min!) + : undefined; - return [min, max]; + return { min, max }; } function _refine(helper: AlgoliaSearchHelper, currentRange: Range) { - return ([nextMin, nextMax]: RangeBoundaries = [undefined, undefined]) => { + return ({ min: nextMin, max: nextMax }: Partial = {}) => { const refinedState = getRefinedState( helper, currentRange, @@ -360,7 +366,7 @@ export const connectRange: RangeConnector = function connectRange( }; const currentRange = _getCurrentRange(stats); - const start = _getCurrentRefinement(helper); + const currentRefinement = _getCurrentRefinement(helper, currentRange); let refine: ReturnType; @@ -386,7 +392,7 @@ export const connectRange: RangeConnector = function connectRange( ...widgetParams, precision, }, - start, + currentRefinement, }; }, diff --git a/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx b/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx index 0c54d0fa7a..d17a470fad 100644 --- a/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx +++ b/packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx @@ -5,7 +5,7 @@ import { h, Component } from 'preact'; import Template from '../Template/Template'; -import type { Range, RangeBoundaries } from '../../connectors'; +import type { Range } from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { RangeInputCSSClasses, @@ -26,7 +26,7 @@ export type RangeInputProps = { templateProps: { templates: RangeInputComponentTemplates; }; - refine: (rangeValue: RangeBoundaries) => void; + refine: (rangeValue: Range) => void; }; // Strips leading `0` from a positive number value @@ -62,10 +62,10 @@ class RangeInput extends Component< event.preventDefault(); const { min, max } = this.state; - this.props.refine([ - min ? Number(min) : undefined, - max ? Number(max) : undefined, - ]); + this.props.refine({ + min: min ? Number(min) : undefined, + max: max ? Number(max) : undefined, + }); }; public render() { diff --git a/packages/instantsearch.js/src/components/RangeInput/__tests__/RangeInput-test.tsx b/packages/instantsearch.js/src/components/RangeInput/__tests__/RangeInput-test.tsx index a264cbfcf6..b1115e60ae 100644 --- a/packages/instantsearch.js/src/components/RangeInput/__tests__/RangeInput-test.tsx +++ b/packages/instantsearch.js/src/components/RangeInput/__tests__/RangeInput-test.tsx @@ -176,7 +176,7 @@ describe('RangeInput', () => { fireEvent.submit(form); } - expect(props.refine).toHaveBeenCalledWith([20, 480]); + expect(props.refine).toHaveBeenCalledWith({ min: 20, max: 480 }); }); it('expect to call refine with min, max as float', () => { @@ -202,7 +202,7 @@ describe('RangeInput', () => { fireEvent.submit(form); } - expect(props.refine).toHaveBeenCalledWith([20.05, 480.05]); + expect(props.refine).toHaveBeenCalledWith({ min: 20.05, max: 480.05 }); }); it('expect to call refine with min only', () => { @@ -225,7 +225,7 @@ describe('RangeInput', () => { fireEvent.submit(form); } - expect(props.refine).toHaveBeenCalledWith([20, undefined]); + expect(props.refine).toHaveBeenCalledWith({ min: 20, max: undefined }); }); it('expect to call refine with max only', () => { @@ -248,7 +248,7 @@ describe('RangeInput', () => { fireEvent.submit(form); } - expect(props.refine).toHaveBeenCalledWith([undefined, 480]); + expect(props.refine).toHaveBeenCalledWith({ min: undefined, max: 480 }); }); it('expect to call refine without values', () => { @@ -263,7 +263,10 @@ describe('RangeInput', () => { fireEvent.submit(form); } - expect(props.refine).toHaveBeenCalledWith([undefined, undefined]); + expect(props.refine).toHaveBeenCalledWith({ + min: undefined, + max: undefined, + }); }); }); }); diff --git a/packages/instantsearch.js/src/components/Slider/Slider.tsx b/packages/instantsearch.js/src/components/Slider/Slider.tsx index 008ff82c0d..733ecfd318 100644 --- a/packages/instantsearch.js/src/components/Slider/Slider.tsx +++ b/packages/instantsearch.js/src/components/Slider/Slider.tsx @@ -8,7 +8,7 @@ import { range } from '../../lib/utils'; import Pit from './Pit'; import Rheostat from './Rheostat'; -import type { RangeBoundaries } from '../../connectors'; +import type { Range } from '../../connectors'; import type { ComponentCSSClasses } from '../../types'; import type { RangeSliderCssClasses, @@ -20,10 +20,10 @@ export type RangeSliderComponentCSSClasses = ComponentCSSClasses; export type SliderProps = { - refine: (values: RangeBoundaries) => void; + refine: (values: Range) => void; min?: number; max?: number; - values: RangeBoundaries; + values: Range; pips?: boolean; step?: number; tooltips?: RangeSliderWidgetParams['tooltips']; @@ -35,9 +35,11 @@ class Slider extends Component { return this.props.min! >= this.props.max!; } - private handleChange = ({ values }: { values: RangeBoundaries }) => { + private handleChange = ({ + values: [min, max], + }: Parameters>[0]) => { if (!this.isDisabled) { - this.props.refine(values); + this.props.refine({ min, max }); } }; @@ -132,7 +134,12 @@ class Slider extends Component { pitPoints={pitPoints} snap={true} snapPoints={snapPoints} - values={(this.isDisabled ? [min, max] : values) as [number, number]} + values={ + (this.isDisabled ? [min, max] : [values.min, values.max]) as [ + number, + number + ] + } disabled={this.isDisabled} /> diff --git a/packages/instantsearch.js/src/components/Slider/__tests__/Slider-test.tsx b/packages/instantsearch.js/src/components/Slider/__tests__/Slider-test.tsx index e0f48af9e2..415b4a53c4 100644 --- a/packages/instantsearch.js/src/components/Slider/__tests__/Slider-test.tsx +++ b/packages/instantsearch.js/src/components/Slider/__tests__/Slider-test.tsx @@ -17,7 +17,7 @@ describe('Slider', () => { refine={() => undefined} min={0} max={500} - values={[0, 0]} + values={{ min: 0, max: 0 }} pips={true} step={2} tooltips={true} @@ -37,7 +37,7 @@ describe('Slider', () => { refine={() => undefined} min={0} max={500} - values={[0, 0]} + values={{ min: 0, max: 0 }} pips={false} step={2} tooltips={true} @@ -57,7 +57,7 @@ describe('Slider', () => { refine={() => undefined} min={0} max={500} - values={[0, 0]} + values={{ min: 0, max: 0 }} pips={false} step={2} tooltips={true} @@ -76,7 +76,7 @@ describe('Slider', () => { refine: jest.fn(), min: 0, max: 500, - values: [0, 0], + values: { min: 0, max: 0 }, pips: true, step: 2, tooltips: true, @@ -92,6 +92,6 @@ describe('Slider', () => { Rheostat.props().onChange!({ values: [0, 100] }); expect(props.refine).toHaveBeenCalledTimes(1); - expect(props.refine).toHaveBeenCalledWith([0, 100]); + expect(props.refine).toHaveBeenCalledWith({ min: 0, max: 100 }); }); }); diff --git a/packages/instantsearch.js/src/connectors/index.ts b/packages/instantsearch.js/src/connectors/index.ts index 6ed5e8ca29..2ab1ed01ea 100644 --- a/packages/instantsearch.js/src/connectors/index.ts +++ b/packages/instantsearch.js/src/connectors/index.ts @@ -160,7 +160,6 @@ export type { RangeWidgetDescription, RangeConnectorParams, Range, - RangeBoundaries, } from 'instantsearch-core'; export { connectRatingMenu } from 'instantsearch-core'; diff --git a/packages/instantsearch.js/src/widgets/range-input/range-input.tsx b/packages/instantsearch.js/src/widgets/range-input/range-input.tsx index e3017c93a1..8b9679e6b8 100644 --- a/packages/instantsearch.js/src/widgets/range-input/range-input.tsx +++ b/packages/instantsearch.js/src/widgets/range-input/range-input.tsx @@ -134,7 +134,7 @@ const renderer = }; templates: RangeInputTemplates; }): Renderer> => - ({ refine, range, start, widgetParams }, isFirstRendering) => { + ({ refine, range, currentRefinement, widgetParams }, isFirstRendering) => { if (isFirstRendering) { renderState.templateProps = prepareTemplateProps({ defaultTemplates, @@ -144,23 +144,15 @@ const renderer = } const { min: rangeMin, max: rangeMax } = range; - const [minValue, maxValue] = start; const step = 1 / Math.pow(10, widgetParams.precision || 0); - const values = { - min: - minValue !== -Infinity && minValue !== rangeMin ? minValue : undefined, - max: - maxValue !== Infinity && maxValue !== rangeMax ? maxValue : undefined, - }; - render( { type SliderProps = { max: number; min: number; - values: [number, number]; + values: Range; }; function createFacetStatsResults({ @@ -385,7 +385,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-slide uiState: {}, }); helper.setState(state0); - refine([stats!.min + 1, stats!.max]); + refine({ min: stats!.min + 1, max: stats!.max }); const state1 = helper.state; expect(helper.search).toHaveBeenCalledTimes(1); @@ -407,7 +407,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-slide uiState: {}, }); helper.setState(state0); - refine([stats!.min, stats!.max - 1]); + refine({ min: stats!.min, max: stats!.max - 1 }); const state1 = helper.state; expect(helper.search).toHaveBeenCalledTimes(1); @@ -427,7 +427,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-slide ); const state0 = helper.state; - refine([stats!.min + 1, stats!.max - 1]); + refine({ min: stats!.min + 1, max: stats!.max - 1 }); const state1 = helper.state; expect(helper.search).toHaveBeenCalledTimes(1); @@ -437,46 +437,6 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/range-slide .addNumericRefinement(attribute, '<=', 4999) ); }); - - it("expect to clamp the min value to the max range when it's greater than range", () => { - widget = rangeSlider({ - container, - attribute, - step: 1, - cssClasses: { root: '' }, - }); - - widget.init!(createInitOptions({ helper, instantSearchInstance })); - - helper.addNumericRefinement(attribute, '>=', 5550); - helper.addNumericRefinement(attribute, '<=', 6000); - - widget.render!(createRenderOptions({ results, helper })); - - const firstRender = render.mock.calls[0][0] as VNode; - - expect((firstRender.props as SliderProps).values[0]).toBe(5000); - }); - - it("expect to clamp the max value to the min range when it's lower than range", () => { - widget = rangeSlider({ - container, - attribute, - step: 1, - cssClasses: { root: '' }, - }); - - widget.init!(createInitOptions({ helper, instantSearchInstance })); - - helper.addNumericRefinement(attribute, '>=', -50); - helper.addNumericRefinement(attribute, '<=', 0); - - widget.render!(createRenderOptions({ results, helper })); - - const firstRender = render.mock.calls[0][0] as VNode; - - expect((firstRender.props as SliderProps).values[1]).toBe(1); - }); }); }); }); diff --git a/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx b/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx index fa8569f2ff..d1961596d6 100644 --- a/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx +++ b/packages/instantsearch.js/src/widgets/range-slider/range-slider.tsx @@ -13,7 +13,6 @@ import { import type { RangeSliderComponentCSSClasses } from '../../components/Slider/Slider'; import type { - RangeBoundaries, RangeConnectorParams, RangeRenderState, RangeWidgetDescription, @@ -37,25 +36,14 @@ const renderer = step?: number; tooltips: RangeSliderWidgetParams['tooltips']; }): Renderer> => - ({ refine, range, start }, isFirstRendering) => { + ({ refine, range, currentRefinement }, isFirstRendering) => { if (isFirstRendering) { // There's no information at this point, let's render nothing. return; } const { min: minRange, max: maxRange } = range; - - const [minStart, maxStart] = start; - const minFinite = minStart === -Infinity ? minRange : minStart; - const maxFinite = maxStart === Infinity ? maxRange : maxStart; - - // Clamp values to the range for avoid extra rendering & refinement - // Should probably be done on the connector side, but we need to stay - // backward compatible so we still need to pass [-Infinity, Infinity] - const values: RangeBoundaries = [ - minFinite! > maxRange! ? maxRange : minFinite, - maxFinite! < minRange! ? minRange : maxFinite, - ]; + const { min: minValue, max: maxValue } = currentRefinement; render( { }, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [-Infinity, Infinity], + currentRefinement: { min: undefined, max: undefined }, }); await waitForNextUpdate(); @@ -44,7 +44,7 @@ describe('useRange', () => { }, refine: expect.any(Function), sendEvent: expect.any(Function), - start: [-Infinity, Infinity], + currentRefinement: { min: undefined, max: undefined }, }); }); }); diff --git a/packages/react-instantsearch/src/ui/RangeInput.tsx b/packages/react-instantsearch/src/ui/RangeInput.tsx index ffc737cde7..90f0ff6817 100644 --- a/packages/react-instantsearch/src/ui/RangeInput.tsx +++ b/packages/react-instantsearch/src/ui/RangeInput.tsx @@ -6,7 +6,7 @@ import type { useRange } from 'react-instantsearch-core'; type RangeRenderState = ReturnType; export type RangeInputProps = Omit, 'onSubmit'> & - Pick & { + Pick & { classNames?: Partial; disabled: boolean; onSubmit: RangeRenderState['refine']; @@ -74,8 +74,8 @@ function stripLeadingZeroFromInput(value: string): string { export function RangeInput({ classNames = {}, - range: { min, max }, - start: [minValue, maxValue], + range: { min: minRange, max: maxRange }, + currentRefinement: { min: minValue, max: maxValue }, step = 1, disabled, onSubmit, @@ -83,14 +83,8 @@ export function RangeInput({ ...props }: RangeInputProps) { const values = { - min: - minValue !== -Infinity && minValue !== min - ? minValue - : unsetNumberInputValue, - max: - maxValue !== Infinity && maxValue !== max - ? maxValue - : unsetNumberInputValue, + min: minValue ?? unsetNumberInputValue, + max: maxValue ?? unsetNumberInputValue, }; const [prevValues, setPrevValues] = useState(values); @@ -118,10 +112,10 @@ export function RangeInput({ className={cx('ais-RangeInput-form', classNames.form)} onSubmit={(event) => { event.preventDefault(); - onSubmit([ - from ? Number(from) : undefined, - to ? Number(to) : undefined, - ]); + onSubmit({ + min: from ? Number(from) : undefined, + max: to ? Number(to) : undefined, + }); }} > @@ -44,7 +44,7 @@ :min="state.range.min" :max="state.range.max" :placeholder="state.range.max" - :value="values.max" + :value="state.currentRefinement.max" @change="maxInput = $event.currentTarget.value" /> @@ -120,19 +120,6 @@ export default { step() { return 1 / Math.pow(10, this.precision); }, - values() { - const [minValue, maxValue] = this.state.start; - const { min: minRange, max: maxRange } = this.state.range; - - return { - min: - minValue !== -Infinity && minValue !== minRange - ? minValue - : undefined, - max: - maxValue !== Infinity && maxValue !== maxRange ? maxValue : undefined, - }; - }, }, methods: { pick(first, second) { @@ -143,7 +130,7 @@ export default { } }, refine({ min, max }) { - this.state.refine([min, max]); + this.state.refine({ min, max }); }, }, }; diff --git a/packages/vue-instantsearch/src/components/__tests__/RangeInput.js b/packages/vue-instantsearch/src/components/__tests__/RangeInput.js index cd029199df..2ff71c2ad6 100644 --- a/packages/vue-instantsearch/src/components/__tests__/RangeInput.js +++ b/packages/vue-instantsearch/src/components/__tests__/RangeInput.js @@ -16,7 +16,7 @@ const defaultRange = { }; const defaultState = { - start: [0, 1000], + currentRefinement: { min: 0, max: 1000 }, range: defaultRange, canRefine: true, refine: () => {},