Skip to content

Commit 19a1247

Browse files
authored
fix: preserve controlled Slider invalid state (#22152)
1 parent 7b66202 commit 19a1247

2 files changed

Lines changed: 171 additions & 15 deletions

File tree

packages/react/src/components/Slider/Slider.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -705,11 +705,20 @@ const Slider = (props: SliderProps) => {
705705
// Set needsOnRelease flag so event fires on next update.
706706
setState({
707707
needsOnRelease: true,
708-
isValid: true,
709-
isValidUpper: true,
710708
});
711709
};
712710

711+
const getValidityUpdateForHandle = (
712+
handle: HandlePosition,
713+
validity: boolean
714+
) => {
715+
if (typeof invalid !== 'undefined') return {};
716+
717+
return handle === HandlePosition.UPPER
718+
? { isValidUpper: validity }
719+
: { isValid: validity };
720+
};
721+
713722
// TODO: Rename this reference.
714723
/**
715724
* Handles a "drag" event by recalculating the value/thumb and setting state
@@ -751,7 +760,7 @@ const Slider = (props: SliderProps) => {
751760
setState({
752761
value: nearestStepValue(value),
753762
left,
754-
isValid: true,
763+
...getValidityUpdateForHandle(HandlePosition.LOWER, true),
755764
});
756765
}
757766
// TODO: Investigate if it would be better to not call `setState`
@@ -826,7 +835,7 @@ const Slider = (props: SliderProps) => {
826835
setState({
827836
value: nearestStepValue(value),
828837
left,
829-
isValid: true,
838+
...getValidityUpdateForHandle(HandlePosition.LOWER, true),
830839
});
831840
}
832841
setState({ correctedValue: null, correctedPosition: null });
@@ -925,12 +934,12 @@ const Slider = (props: SliderProps) => {
925934
| HandlePosition
926935
| undefined;
927936

928-
if (handlePosition === HandlePosition.LOWER) {
929-
setState({ isValid: validity });
930-
} else if (handlePosition === HandlePosition.UPPER) {
931-
setState({ isValidUpper: validity });
932-
}
933-
setState({ isValid: validity });
937+
setState(
938+
getValidityUpdateForHandle(
939+
handlePosition ?? HandlePosition.LOWER,
940+
validity
941+
)
942+
);
934943

935944
if (validity) {
936945
const adjustedValue = handlePosition
@@ -1101,13 +1110,13 @@ const Slider = (props: SliderProps) => {
11011110
setState({
11021111
value: valueUpper && newValue > valueUpper ? valueUpper : newValue,
11031112
left: valueUpper && newValue > valueUpper ? leftUpper : newLeft,
1104-
isValid: true,
1113+
...getValidityUpdateForHandle(handle, true),
11051114
});
11061115
} else {
11071116
setState({
11081117
valueUpper: value && newValue < value ? value : newValue,
11091118
leftUpper: value && newValue < value ? left : newLeft,
1110-
isValidUpper: true,
1119+
...getValidityUpdateForHandle(handle, true),
11111120
});
11121121
}
11131122
};
@@ -1123,7 +1132,7 @@ const Slider = (props: SliderProps) => {
11231132
// @ts-expect-error - Passing a string to something that expects a
11241133
// number.
11251134
value,
1126-
isValid: true,
1135+
...getValidityUpdateForHandle(handle, true),
11271136
});
11281137
} else {
11291138
setState({
@@ -1132,7 +1141,7 @@ const Slider = (props: SliderProps) => {
11321141
// @ts-expect-error - Passing a string to something that expects a
11331142
// number.
11341143
valueUpper: value,
1135-
isValidUpper: true,
1144+
...getValidityUpdateForHandle(handle, true),
11361145
});
11371146
}
11381147
};

packages/react/src/components/Slider/__tests__/Slider-test.js

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import React from 'react';
8+
import React, { useState } from 'react';
99
import Slider from '../Slider';
1010
import userEvent from '@testing-library/user-event';
1111
import {
@@ -267,6 +267,153 @@ describe('Slider', () => {
267267
expect(onChange).toHaveBeenLastCalledWith({ value: 999 });
268268
});
269269

270+
it('should keep controlled invalid state after dragging to another invalid value', async () => {
271+
const ControlledSlider = () => {
272+
const [value, setValue] = useState(20);
273+
274+
return (
275+
<Slider
276+
labelText="Slider"
277+
value={value}
278+
min={0}
279+
max={100}
280+
ariaLabelInput={inputAriaValue}
281+
invalid={value > 25}
282+
invalidText="Error message"
283+
onChange={({ value }) => setValue(value)}
284+
/>
285+
);
286+
};
287+
288+
render(<ControlledSlider />);
289+
290+
const inputElement = screen.getByLabelText(inputAriaValue);
291+
const slider = screen.getByRole('slider');
292+
const sliderRoot = screen.getByRole('presentation');
293+
294+
jest
295+
.spyOn(sliderRoot, 'getBoundingClientRect')
296+
.mockImplementation(() => createDOMRect({ left: 0, width: 100 }));
297+
298+
await userEvent.clear(inputElement);
299+
await userEvent.type(inputElement, '30');
300+
301+
expect(inputElement).toHaveAttribute('aria-invalid', 'true');
302+
expect(screen.getByText('Error message')).toBeInTheDocument();
303+
304+
fireEvent.mouseDown(slider, { clientX: 35 });
305+
fireEvent.mouseUp(document);
306+
307+
await waitFor(() => {
308+
expect(slider).toHaveAttribute('aria-valuenow', '35');
309+
});
310+
311+
expect(screen.getByLabelText(inputAriaValue)).toHaveAttribute(
312+
'aria-invalid',
313+
'true'
314+
);
315+
expect(screen.getByText('Error message')).toBeInTheDocument();
316+
});
317+
318+
it('should keep controlled invalid state after keyboard interaction changes to another invalid value', async () => {
319+
const ControlledSlider = () => {
320+
const [value, setValue] = useState(20);
321+
322+
return (
323+
<Slider
324+
labelText="Slider"
325+
value={value}
326+
min={0}
327+
max={100}
328+
ariaLabelInput={inputAriaValue}
329+
invalid={value > 25}
330+
invalidText="Error message"
331+
onChange={({ value }) => setValue(value)}
332+
/>
333+
);
334+
};
335+
336+
render(<ControlledSlider />);
337+
338+
const inputElement = screen.getByLabelText(inputAriaValue);
339+
const slider = screen.getByRole('slider');
340+
341+
await userEvent.clear(inputElement);
342+
await userEvent.type(inputElement, '30');
343+
344+
expect(inputElement).toHaveAttribute('aria-invalid', 'true');
345+
expect(screen.getByText('Error message')).toBeInTheDocument();
346+
347+
await userEvent.click(slider);
348+
await userEvent.keyboard('{ArrowRight}');
349+
350+
await waitFor(() => {
351+
expect(slider).toHaveAttribute('aria-valuenow', '31');
352+
});
353+
354+
expect(screen.getByLabelText(inputAriaValue)).toHaveAttribute(
355+
'aria-invalid',
356+
'true'
357+
);
358+
expect(screen.getByText('Error message')).toBeInTheDocument();
359+
});
360+
361+
it('should keep controlled invalid state for the upper handle after dragging to another invalid value', async () => {
362+
const ControlledRangeSlider = () => {
363+
const [value, setValue] = useState(20);
364+
const [valueUpper, setValueUpper] = useState(70);
365+
366+
return (
367+
<Slider
368+
labelText="Slider"
369+
value={value}
370+
unstable_valueUpper={valueUpper}
371+
min={0}
372+
max={100}
373+
ariaLabelInput={defaultAriaLabelInput}
374+
unstable_ariaLabelInputUpper={defaultAriaLabelInputUpper}
375+
invalid={valueUpper > 75}
376+
invalidText="Error message"
377+
onChange={({ value, valueUpper }) => {
378+
setValue(value);
379+
if (typeof valueUpper !== 'undefined') {
380+
setValueUpper(valueUpper);
381+
}
382+
}}
383+
/>
384+
);
385+
};
386+
387+
render(<ControlledRangeSlider />);
388+
389+
const upperInput = screen.getByLabelText(defaultAriaLabelInputUpper, {
390+
selector: 'input',
391+
});
392+
const sliderRoot = screen.getByRole('presentation');
393+
const [lowerThumb, upperThumb] = screen.getAllByRole('slider');
394+
395+
jest
396+
.spyOn(sliderRoot, 'getBoundingClientRect')
397+
.mockImplementation(() => createDOMRect({ left: 0, width: 100 }));
398+
399+
await userEvent.clear(upperInput);
400+
await userEvent.type(upperInput, '80');
401+
402+
expect(upperInput).toHaveAttribute('aria-invalid', 'true');
403+
expect(screen.getByText('Error message')).toBeInTheDocument();
404+
405+
fireEvent.mouseDown(upperThumb, { clientX: 84 });
406+
fireEvent.mouseUp(document);
407+
408+
await waitFor(() => {
409+
expect(upperThumb).toHaveAttribute('aria-valuenow', '84');
410+
});
411+
412+
expect(lowerThumb).toHaveAttribute('aria-valuenow', '20');
413+
expect(upperInput).toHaveAttribute('aria-invalid', 'true');
414+
expect(screen.getByText('Error message')).toBeInTheDocument();
415+
});
416+
270417
it('sets correct state when typing a valid value in input field', async () => {
271418
const { type } = userEvent;
272419
renderSlider({

0 commit comments

Comments
 (0)