Skip to content

Commit e4eae9d

Browse files
sookmaxsnowystingerreidbarber
authored
Slider: Fix controlled value updates (#5655)
* fix controlled Slider value updates * Update values ref when we start dragging * fix lint --------- Co-authored-by: Rob Snow <rsnow@adobe.com> Co-authored-by: Reid Barber <reid@reidbarber.com>
1 parent e592b54 commit e4eae9d

File tree

3 files changed

+107
-40
lines changed

3 files changed

+107
-40
lines changed

packages/@react-stately/slider/src/useSliderState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export function useSliderState<T extends number | number[]>(props: SliderStateOp
198198

199199
const valuesRef = useRef<number[]>(values);
200200
const isDraggingsRef = useRef<boolean[]>(isDraggings);
201+
201202
let setValues = (values: number[]) => {
202203
valuesRef.current = values;
203204
setValuesState(values);
@@ -244,6 +245,9 @@ export function useSliderState<T extends number | number[]>(props: SliderStateOp
244245
if (isDisabled || !isThumbEditable(index)) {
245246
return;
246247
}
248+
if (dragging) {
249+
valuesRef.current = values;
250+
}
247251

248252
const wasDragging = isDraggingsRef.current[index];
249253
isDraggingsRef.current = replaceIndex(isDraggingsRef.current, index, dragging);

packages/react-aria-components/stories/Slider.stories.tsx

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,46 +18,53 @@ export default {
1818
title: 'React Aria Components'
1919
};
2020

21-
export const SliderExample = () => (
22-
<Slider
23-
data-testid="slider-example"
24-
defaultValue={[30, 60]}
25-
style={{
26-
position: 'relative',
27-
display: 'flex',
28-
flexDirection: 'column',
29-
alignItems: 'center',
30-
width: 300
31-
}}>
32-
<div style={{display: 'flex', alignSelf: 'stretch'}}>
33-
<Label>Test</Label>
34-
<SliderOutput style={{flex: '1 0 auto', textAlign: 'end'}}>
35-
{({state}) => `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`}
36-
</SliderOutput>
37-
</div>
38-
<SliderTrack
39-
style={{
40-
position: 'relative',
41-
height: 30,
42-
width: '100%'
43-
}}>
44-
<div
21+
export const SliderExample = () => {
22+
const [value, setValue] = React.useState([30, 60]);
23+
return (
24+
<div>
25+
<Slider<number[]>
26+
data-testid="slider-example"
27+
value={value}
28+
onChange={setValue}
4529
style={{
46-
position: 'absolute',
47-
backgroundColor: 'gray',
48-
height: 3,
49-
top: 13,
50-
width: '100%'
51-
}} />
52-
<CustomThumb index={0}>
53-
<Label>A</Label>
54-
</CustomThumb>
55-
<CustomThumb index={1}>
56-
<Label>B</Label>
57-
</CustomThumb>
58-
</SliderTrack>
59-
</Slider>
60-
);
30+
position: 'relative',
31+
display: 'flex',
32+
flexDirection: 'column',
33+
alignItems: 'center',
34+
width: 300
35+
}}>
36+
<div style={{display: 'flex', alignSelf: 'stretch'}}>
37+
<Label>Test</Label>
38+
<SliderOutput style={{flex: '1 0 auto', textAlign: 'end'}}>
39+
{({state}) => `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`}
40+
</SliderOutput>
41+
</div>
42+
<SliderTrack
43+
style={{
44+
position: 'relative',
45+
height: 30,
46+
width: '100%'
47+
}}>
48+
<div
49+
style={{
50+
position: 'absolute',
51+
backgroundColor: 'gray',
52+
height: 3,
53+
top: 13,
54+
width: '100%'
55+
}} />
56+
<CustomThumb index={0}>
57+
<Label>A</Label>
58+
</CustomThumb>
59+
<CustomThumb index={1}>
60+
<Label>B</Label>
61+
</CustomThumb>
62+
</SliderTrack>
63+
</Slider>
64+
<button onClick={() => setValue([0, 100])}>reset</button>
65+
</div>
66+
);
67+
};
6168

6269
export const SliderCSS = (props: SliderProps) => (
6370
<Slider {...props} defaultValue={30} className={styles.slider}>

packages/react-aria-components/test/Slider.test.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {fireEvent, pointerMap, render} from '@react-spectrum/test-utils';
1414
import {Label, Slider, SliderContext, SliderOutput, SliderThumb, SliderTrack} from '../';
15-
import React from 'react';
15+
import React, {useState} from 'react';
1616
import userEvent from '@testing-library/user-event';
1717

1818
let TestSlider = ({sliderProps, thumbProps, trackProps, outputProps}) => (
@@ -209,6 +209,62 @@ describe('Slider', () => {
209209
expect(output).toHaveTextContent('30 – 60');
210210
});
211211

212+
it('should support multiple thumbs (controlled)', async () => {
213+
function SliderClient() {
214+
const [value, setValue] = useState([30, 60]);
215+
return (<div>
216+
<Slider value={value} onChange={setValue}>
217+
<Label>Test</Label>
218+
<SliderOutput>
219+
{({state}) => state.values.map((_, i) => state.getThumbValueLabel(i)).join(' – ')}
220+
</SliderOutput>
221+
<SliderTrack>
222+
{({state}) => state.values.map((_, i) => <SliderThumb key={i} index={i} className="thumb" />)}
223+
</SliderTrack>
224+
</Slider>
225+
<button data-testid="reset-button" onClick={() => setValue([0, 100])}>reset</button>
226+
</div>);
227+
}
228+
229+
let {getAllByRole, getByTestId} = render(<SliderClient />);
230+
231+
let sliders = getAllByRole('slider');
232+
233+
expect(sliders).toHaveLength(2);
234+
expect(sliders[0]).toHaveValue('30');
235+
expect(sliders[1]).toHaveValue('60');
236+
237+
let resetButton = getByTestId('reset-button');
238+
await user.click(resetButton);
239+
expect(sliders[0]).toHaveValue('0');
240+
expect(sliders[1]).toHaveValue('100');
241+
242+
await user.tab(); // body (because we've clicked the reset button?)
243+
await user.tab();
244+
expect(document.activeElement).toBe(sliders[0]);
245+
246+
await user.keyboard('{ArrowRight}');
247+
await user.keyboard('{ArrowRight}');
248+
await user.keyboard('{ArrowRight}');
249+
expect(sliders[0]).toHaveValue('3');
250+
expect(sliders[1]).toHaveValue('100');
251+
252+
await user.click(resetButton);
253+
expect(sliders[0]).toHaveValue('0');
254+
expect(sliders[1]).toHaveValue('100');
255+
256+
await user.tab(); // body
257+
await user.tab(); // sliders[0]
258+
await user.tab();
259+
expect(document.activeElement).toBe(sliders[1]);
260+
261+
await user.keyboard('{ArrowLeft}');
262+
await user.keyboard('{ArrowLeft}');
263+
await user.keyboard('{ArrowLeft}');
264+
expect(sliders[0]).toHaveValue('0');
265+
expect(sliders[1]).toHaveValue('97');
266+
});
267+
212268
it('should support clicking on the track to move the thumb', async () => {
213269
let onChange = jest.fn();
214270
let {getByRole} = renderSlider({onChange});

0 commit comments

Comments
 (0)