Skip to content

Commit 910a0a0

Browse files
Maxime Jantonbobylito
authored andcommitted
feat(range-slider): use rheostat as slider component (#2142)
* feat(range-slider): use `rheostat` as slider component * doc(range-slider): widget documentation * doc(migration): add note about new range slider * refactor(Slider): `_.range` never includes max value * doc(range-slider): missing usage options * fix(range-slider): set default templates
1 parent c7beb1d commit 910a0a0

File tree

11 files changed

+1194
-583
lines changed

11 files changed

+1194
-583
lines changed

docgen/src/guides/migration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ As of now, we consider the engine used to build the widgets in InstantSearch.js
8888
as an implementation detail. Since we do not expose it anymore, we'll be able
8989
to change it and use the best solution for each release.
9090

91+
## RangeSlider widget is using Rheostat as Slider Component
92+
93+
Slider compoments are hard to implement and that's why we rely on an external
94+
component for that. We are taking the opportunity of this new version
95+
to switch to the current state of the art of sliders: [Rheostat](https://github.com/airbnb/rheostat).
96+
97+
If you want to customize the style, some CSS classes have changed. You can find
98+
examples of stylesheets by clicking [here](https://github.com/airbnb/rheostat/tree/master/css).
99+
100+
We are still providing a look which is similar as the one in the V1.
101+
91102
## searchFunction can be used to modify parameters
92103

93104
Introduced in 1.3.0, `searchFunction` was originally meant as a way to modify

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"lodash": "^4.17.4",
109109
"react": "^15.4.2",
110110
"react-dom": "^15.4.2",
111-
"react-nouislider": "^1.14.2",
111+
"rheostat": "^2.1.0",
112112
"to-factory": "^1.0.0"
113113
},
114114
"license": "MIT"

src/components/Slider/Pit.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, {PropTypes} from 'react';
2+
import cx from 'classnames';
3+
4+
const Pit = ({style, children}) => {
5+
// first, end & middle
6+
const positionValue = Math.round(parseFloat(style.left));
7+
const shouldDisplayValue = [0, 50, 100].includes(positionValue);
8+
9+
return (
10+
<div
11+
style={ {...style, marginLeft: positionValue === 100 ? '-2px' : 0} }
12+
className={ cx('ais-range-slider--marker ais-range-slider--marker-horizontal', {
13+
'ais-range-slider--marker-large': shouldDisplayValue,
14+
}) }
15+
>
16+
{ shouldDisplayValue
17+
? <div className="ais-range-slider--value">{ Math.round(children * 100) / 100 }</div>
18+
: null }
19+
</div>
20+
);
21+
};
22+
23+
Pit.propTypes = {
24+
children: PropTypes.oneOfType([
25+
PropTypes.number.isRequired,
26+
PropTypes.string.isRequired,
27+
]),
28+
style: PropTypes.shape({
29+
position: PropTypes.string.isRequired,
30+
left: PropTypes.string.isRequired,
31+
}),
32+
};
33+
34+
export default Pit;

src/components/Slider/Slider.js

Lines changed: 78 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,98 @@
1-
import React from 'react';
2-
import omit from 'lodash/omit';
1+
import times from 'lodash/times';
2+
import range from 'lodash/range';
3+
import has from 'lodash/has';
34

4-
import Nouislider from 'react-nouislider';
5+
import React, {Component, PropTypes} from 'react';
6+
import Rheostat from 'rheostat';
7+
import cx from 'classnames';
58

6-
const cssPrefix = 'ais-range-slider--';
7-
8-
import isEqual from 'lodash/isEqual';
9+
import Pit from './Pit.js';
910

1011
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
1112
import headerFooterHOC from '../../decorators/headerFooter.js';
1213

13-
export class RawSlider extends React.Component {
14-
componentWillMount() {
15-
this.handleChange = this.handleChange.bind(this);
14+
class Slider extends Component {
15+
16+
static propTypes = {
17+
refine: PropTypes.func.isRequired,
18+
min: PropTypes.number.isRequired,
19+
max: PropTypes.number.isRequired,
20+
values: PropTypes.arrayOf(PropTypes.number).isRequired,
21+
pips: PropTypes.oneOfType([
22+
PropTypes.bool,
23+
PropTypes.object,
24+
]),
25+
step: PropTypes.number.isRequired,
26+
tooltips: PropTypes.oneOfType([
27+
PropTypes.bool,
28+
PropTypes.shape({format: PropTypes.func.isRequired}),
29+
]),
1630
}
1731

18-
shouldComponentUpdate(nextProps) {
19-
return !isEqual(this.props.range, nextProps.range) ||
20-
!isEqual(this.props.start, nextProps.start);
32+
handleChange = ({values}) => {
33+
const {refine} = this.props;
34+
refine(values);
2135
}
2236

23-
// we are only interested in rawValues
24-
handleChange(formattedValues, handleId, rawValues) {
25-
this.props.onChange(rawValues);
37+
// creates an array number where to display a pit point on the slider
38+
computeDefaultPitPoints({min, max}) {
39+
const totalLength = max - min;
40+
const steps = 34;
41+
const stepsLength = totalLength / steps;
42+
43+
const pitPoints = [min, ...times(steps - 1, step => min + stepsLength * (step + 1)), max]
44+
// bug with `key={ 0 }` and preact, see https://github.com/developit/preact/issues/642
45+
.map(pitPoint => pitPoint === 0 ? 0.00001 : pitPoint);
46+
47+
return pitPoints;
48+
}
49+
50+
// creates an array of values where the slider should snap to
51+
computeSnapPoints({min, max, step}) {
52+
return [...range(min, max, step), max];
53+
}
54+
55+
createHandleComponent = tooltips => props => {
56+
const value = has(tooltips, 'format')
57+
? tooltips.format(props['aria-valuenow'])
58+
: props['aria-valuenow'];
59+
60+
return (
61+
<div
62+
{...props}
63+
className={ cx('ais-range-slider--handle', props.className)}
64+
>
65+
{ tooltips
66+
? <div className="ais-range-slider--tooltip">{value}</div>
67+
: null }
68+
</div>
69+
);
2670
}
2771

2872
render() {
29-
// display a `disabled` state of the `NoUiSlider` when range.min === range.max
30-
const {range: {min, max}} = this.props;
31-
const isDisabled = min === max;
32-
33-
// when range.min === range.max, we only want to add a little more to the max
34-
// to display the same value in the UI, but the `NoUiSlider` wont
35-
// throw an error since they are not the same value.
36-
const range = isDisabled
37-
? {min, max: min + 0.0001}
38-
: {min, max};
39-
40-
// setup pips
41-
let pips;
42-
if (this.props.pips === false) {
43-
pips = undefined;
44-
} else if (this.props.pips === true || typeof this.props.pips === 'undefined') {
45-
pips = {
46-
mode: 'positions',
47-
density: 3,
48-
values: [0, 50, 100],
49-
stepped: true,
50-
};
51-
} else {
52-
pips = this.props.pips;
53-
}
73+
const {tooltips, step, pips, values, min, max} = this.props;
74+
75+
const snapPoints = this.computeSnapPoints({min, max, step});
76+
const pitPoints = pips === true || pips === undefined || pips === false
77+
? this.computeDefaultPitPoints({min, max})
78+
: pips;
5479

5580
return (
56-
<Nouislider
57-
// NoUiSlider also accepts a cssClasses prop, but we don't want to
58-
// provide one.
59-
{...omit(this.props, ['cssClasses', 'range'])}
60-
animate={false}
61-
behaviour={'snap'}
62-
connect
63-
cssPrefix={cssPrefix}
64-
onChange={this.handleChange}
65-
range={range}
66-
disabled={isDisabled}
67-
pips={pips}
81+
<Rheostat
82+
handle={ this.createHandleComponent(tooltips) }
83+
onChange={ this.handleChange }
84+
min={ min }
85+
max={ max }
86+
pitComponent={ Pit }
87+
pitPoints={ pitPoints }
88+
snap={ true }
89+
snapPoints={ snapPoints }
90+
values={ values }
91+
disabled={ min === max }
6892
/>
6993
);
7094
}
95+
7196
}
7297

73-
RawSlider.propTypes = {
74-
onChange: React.PropTypes.func,
75-
onSlide: React.PropTypes.func,
76-
pips: React.PropTypes.oneOfType([
77-
React.PropTypes.bool,
78-
React.PropTypes.object,
79-
]),
80-
range: React.PropTypes.object.isRequired,
81-
start: React.PropTypes.arrayOf(React.PropTypes.number).isRequired,
82-
tooltips: React.PropTypes.oneOfType([
83-
React.PropTypes.bool,
84-
React.PropTypes.arrayOf(
85-
React.PropTypes.shape({
86-
to: React.PropTypes.func,
87-
})
88-
),
89-
]),
90-
};
91-
92-
export default autoHideContainerHOC(headerFooterHOC(RawSlider));
98+
export default autoHideContainerHOC(headerFooterHOC(Slider));
Lines changed: 17 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,23 @@
1-
import React from 'react';
2-
import expect from 'expect';
3-
import TestUtils from 'react-addons-test-utils';
4-
import expectJSX from 'expect-jsx';
5-
import {RawSlider as Slider} from '../Slider';
6-
import Nouislider from 'react-nouislider';
7-
expect.extend(expectJSX);
8-
9-
describe('Slider', () => {
10-
// to ensure the global.window is set
111

12-
let renderer;
13-
let props;
14-
15-
beforeEach(() => {
16-
const {createRenderer} = TestUtils;
17-
renderer = createRenderer();
18-
19-
props = {
20-
range: {min: 0, max: 5000},
21-
format: {to: () => {}, from: () => {}},
22-
start: [0, 0],
23-
};
24-
});
2+
import React from 'react';
3+
import renderer from 'react-test-renderer';
254

26-
it('should render <NouiSlider {...props} />', () => {
27-
const out = render();
28-
expect(out).toEqualJSX(
29-
<Nouislider
30-
animate={false}
31-
behaviour="snap"
32-
connect
33-
cssPrefix="ais-range-slider--"
34-
format={{to: () => {}, from: () => {}}}
35-
onChange={() => {}}
36-
pips={{
37-
density: 3,
38-
mode: 'positions',
39-
stepped: true,
40-
values: [0, 50, 100],
41-
}}
42-
range={props.range}
43-
start={ props.start }
44-
/>
45-
);
46-
});
5+
import Slider from '../Slider';
476

48-
it('should render <NouisLider disabled="true" /> when ranges are equal', () => {
49-
props.range.min = props.range.max = 8;
50-
const out = render();
51-
expect(out).toEqualJSX(
52-
<Nouislider
53-
animate={ false }
54-
behaviour="snap"
55-
connect
56-
cssPrefix="ais-range-slider--"
57-
format={ {to: () => {}, from: () => {}} }
58-
onChange={ () => {} }
59-
pips={ {
60-
density: 3,
61-
mode: 'positions',
62-
stepped: true,
63-
values: [0, 50, 100],
64-
} }
65-
range={ {min: props.range.min, max: props.range.min + 0.0001} }
66-
start={ props.start }
67-
disabled
7+
describe('Slider', () => {
8+
it('should render correctly', () => {
9+
const tree = renderer.create(
10+
<Slider
11+
refine={ () => undefined }
12+
min={ 0 }
13+
max={ 500 }
14+
values={ [0, 0] }
15+
pips={ true }
16+
step={ 2 }
17+
tooltips={ true }
18+
shouldAutoHideContainer={ false }
6819
/>
69-
);
20+
).toJSON();
21+
expect(tree).toMatchSnapshot();
7022
});
71-
72-
function render() {
73-
renderer.render(<Slider {...props} />);
74-
return renderer.getRenderOutput();
75-
}
7623
});

0 commit comments

Comments
 (0)