Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(rangeinput): allow input of numbers with precision #5541

Merged
merged 8 commits into from
May 11, 2023
Merged
2 changes: 1 addition & 1 deletion examples/react-hooks/default-theme/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function App() {
/>
</Panel>
<Panel header="Price">
<RangeInput attribute="price" />
<RangeInput attribute="price" precision={1} />
</Panel>
<Panel header="Free Shipping">
<ToggleRefinement
Expand Down
19 changes: 19 additions & 0 deletions packages/instantsearch.js/src/__tests__/common.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createMenuTests,
createInfiniteHitsTests,
createHitsTests,
createRangeInputTests,
} from '@instantsearch/tests';

import instantsearch from '../index.es';
Expand All @@ -22,6 +23,7 @@ import {
searchBox,
hits,
index,
rangeInput,
} from '../widgets';

createHierarchicalMenuTests(({ instantSearchOptions, widgetParams }) => {
Expand Down Expand Up @@ -243,3 +245,20 @@ createHitsTests(({ instantSearchOptions, widgetParams }) => {
})
.start();
});

createRangeInputTests(({ instantSearchOptions, widgetParams }) => {
instantsearch(instantSearchOptions)
.addWidgets([
rangeInput({
container: document.body.appendChild(document.createElement('div')),
...widgetParams,
}),
])
.on('error', () => {
/*
* prevent rethrowing InstantSearch errors, so tests can be asserted.
* IRL this isn't needed, as the error doesn't stop execution.
*/
})
.start();
});
32 changes: 22 additions & 10 deletions packages/instantsearch.js/src/components/RangeInput/RangeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,43 @@ export type RangeInputProps = {
refine: (rangeValue: RangeBoundaries) => void;
};

class RangeInput extends Component<RangeInputProps, Partial<Range>> {
// Strips leading `0` from a positive number value
function stripLeadingZeroFromInput(value: string): string {
return value.replace(/^(0+)\d/, (part) => Number(part).toString());
}

class RangeInput extends Component<
RangeInputProps,
{ min?: string; max?: string }
> {
public state = {
min: this.props.values.min,
max: this.props.values.max,
min: this.props.values.min?.toString(),
max: this.props.values.max?.toString(),
};

public componentWillReceiveProps(nextProps: RangeInputProps) {
this.setState({
min: nextProps.values.min,
max: nextProps.values.max,
min: nextProps.values.min?.toString(),
max: nextProps.values.max?.toString(),
});
}

private onInput = (key: string) => (event: Event) => {
private onInput = (key: keyof typeof this.state) => (event: Event) => {
const { value } = event.currentTarget as HTMLInputElement;

this.setState({
[key]: Number(value),
[key]: value,
});
};

private onSubmit = (event: Event) => {
event.preventDefault();

this.props.refine([this.state.min, this.state.max]);
const { min, max } = this.state;
this.props.refine([
min ? Number(min) : undefined,
max ? Number(max) : undefined,
]);
};

public render() {
Expand All @@ -79,7 +91,7 @@ class RangeInput extends Component<RangeInputProps, Partial<Range>> {
min={min}
max={max}
step={step}
value={minValue ?? ''}
value={stripLeadingZeroFromInput(minValue ?? '')}
onInput={this.onInput('min')}
placeholder={min?.toString()}
disabled={isDisabled}
Expand All @@ -102,7 +114,7 @@ class RangeInput extends Component<RangeInputProps, Partial<Range>> {
min={min}
max={max}
step={step}
value={maxValue ?? ''}
value={stripLeadingZeroFromInput(maxValue ?? '')}
onInput={this.onInput('max')}
placeholder={max?.toString()}
disabled={isDisabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ describe('RangeInput', () => {

expect(component).toMatchSnapshot();
expect(component.state()).toEqual({
min: 20,
max: 480,
min: '20',
max: '480',
});
});

Expand Down Expand Up @@ -92,8 +92,8 @@ describe('RangeInput', () => {

expect(component).toMatchSnapshot();
expect(component.state()).toEqual({
min: 20,
max: 480,
min: '20',
max: '480',
});
});

Expand All @@ -120,8 +120,8 @@ describe('RangeInput', () => {

expect(component).toMatchSnapshot();
expect(component.state()).toEqual({
min: 20,
max: 480,
min: '20',
max: '480',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ exports[`RangeInput expect to render with values 1`] = `
placeholder="0"
step={1}
type="number"
value={20}
value="20"
/>
</label>
<Template
Expand Down Expand Up @@ -213,7 +213,7 @@ exports[`RangeInput expect to render with values 1`] = `
placeholder="500"
step={1}
type="number"
value={480}
value="480"
/>
</label>
<Template
Expand Down Expand Up @@ -340,7 +340,7 @@ exports[`RangeInput willReceiveProps expect to update the empty state from given
placeholder="0"
step={1}
type="number"
value={20}
value="20"
/>
</label>
<Template
Expand Down Expand Up @@ -373,7 +373,7 @@ exports[`RangeInput willReceiveProps expect to update the empty state from given
placeholder="500"
step={1}
type="number"
value={480}
value="480"
/>
</label>
<Template
Expand Down Expand Up @@ -420,7 +420,7 @@ exports[`RangeInput willReceiveProps expect to update the state from given props
placeholder="0"
step={1}
type="number"
value={40}
value="40"
/>
</label>
<Template
Expand Down Expand Up @@ -453,7 +453,7 @@ exports[`RangeInput willReceiveProps expect to update the state from given props
placeholder="500"
step={1}
type="number"
value={460}
value="460"
/>
</label>
<Template
Expand Down Expand Up @@ -500,7 +500,7 @@ exports[`RangeInput willReceiveProps expect to update the state from given props
placeholder="0"
step={1}
type="number"
value={20}
value="20"
/>
</label>
<Template
Expand Down Expand Up @@ -533,7 +533,7 @@ exports[`RangeInput willReceiveProps expect to update the state from given props
placeholder="500"
step={1}
type="number"
value={480}
value="480"
/>
</label>
<Template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createPaginationTests,
createInfiniteHitsTests,
createHitsTests,
createRangeInputTests,
} from '@instantsearch/tests';
import { act, render } from '@testing-library/react';
import React from 'react';
Expand All @@ -25,6 +26,7 @@ import {
useInstantSearch,
Hits,
Index,
RangeInput,
} from '..';

import type { Hit } from 'instantsearch.js';
Expand Down Expand Up @@ -204,3 +206,12 @@ createHitsTests(({ instantSearchOptions, widgetParams }) => {
</InstantSearch>
);
}, act);

createRangeInputTests(({ instantSearchOptions, widgetParams }) => {
render(
<InstantSearch {...instantSearchOptions}>
<RangeInput {...widgetParams} />
<GlobalErrorSwallower />
</InstantSearch>
);
}, act);
42 changes: 29 additions & 13 deletions packages/react-instantsearch-hooks-web/src/ui/RangeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ export type RangeInputTranslations = {
};

// if the default value is undefined, React considers the component uncontrolled initially, which we don't want 0 or NaN as the default value
const unsetNumberInputValue = '' as unknown as number;
const unsetNumberInputValue = '';

// Strips leading `0` from a positive number value
function stripLeadingZeroFromInput(value: string): string {
return value.replace(/^(0+)\d/, (part) => Number(part).toString());
}

export function RangeInput({
classNames = {},
Expand All @@ -91,12 +96,12 @@ export function RangeInput({
const [prevValues, setPrevValues] = useState(values);

const [{ from, to }, setRange] = useState({
from: values.min,
to: values.max,
from: values.min?.toString(),
to: values.max?.toString(),
});

if (values.min !== prevValues.min || values.max !== prevValues.max) {
setRange({ from: values.min, to: values.max });
setRange({ from: values.min?.toString(), to: values.max?.toString() });
setPrevValues(values);
}

Expand All @@ -114,7 +119,10 @@ export function RangeInput({
className={cx('ais-RangeInput-form', classNames.form)}
onSubmit={(event) => {
event.preventDefault();
onSubmit([from, to]);
onSubmit([
from ? Number(from) : undefined,
to ? Number(to) : undefined,
]);
}}
>
<label className={cx('ais-RangeInput-label', classNames.label)}>
Expand All @@ -128,13 +136,17 @@ export function RangeInput({
type="number"
min={min}
max={max}
value={from?.toString()} // Strips leading `0` from a positive number value
value={stripLeadingZeroFromInput(from || unsetNumberInputValue)}
step={step}
placeholder={min?.toString()}
disabled={disabled}
onInput={({ currentTarget }) =>
setRange({ from: Number(currentTarget.value), to })
}
onInput={({ currentTarget }) => {
const value = currentTarget.value;
setRange({
from: value || unsetNumberInputValue,
to,
});
}}
/>
</label>
<span className={cx('ais-RangeInput-separator', classNames.separator)}>
Expand All @@ -151,13 +163,17 @@ export function RangeInput({
type="number"
min={min}
max={max}
value={to?.toString()} // Strips leading `0` from a positive number value
value={stripLeadingZeroFromInput(to || unsetNumberInputValue)}
step={step}
placeholder={max?.toString()}
disabled={disabled}
onInput={({ currentTarget }) =>
setRange({ from, to: Number(currentTarget.value) })
}
onInput={({ currentTarget }) => {
const value = currentTarget.value;
setRange({
from,
to: value || unsetNumberInputValue,
});
}}
/>
</label>
<button
Expand Down
18 changes: 18 additions & 0 deletions packages/vue-instantsearch/src/__tests__/common.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createPaginationTests,
createInfiniteHitsTests,
createHitsTests,
createRangeInputTests,
} from '@instantsearch/tests';

import { nextTick, mountApp } from '../../test/utils';
Expand All @@ -25,6 +26,7 @@ import {
createWidgetMixin,
AisHits,
AisIndex,
AisRangeInput,
} from '../instantsearch';
jest.unmock('instantsearch.js/es');

Expand Down Expand Up @@ -280,3 +282,19 @@ createHitsTests(async ({ instantSearchOptions, widgetParams }) => {

await nextTick();
});

createRangeInputTests(async ({ instantSearchOptions, widgetParams }) => {
mountApp(
{
render: renderCompat((h) =>
h(AisInstantSearch, { props: instantSearchOptions }, [
h(AisRangeInput, { props: widgetParams }),
h(GlobalErrorSwallower),
])
),
},
document.body.appendChild(document.createElement('div'))
);

await nextTick();
});
1 change: 1 addition & 0 deletions tests/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './widgets/menu';
export * from './widgets/pagination';
export * from './widgets/infinite-hits';
export * from './widgets/hits';
export * from './widgets/range-input';
Loading