Skip to content

Commit 65c6ec7

Browse files
committed
Use an extra hidden input for field integration
1 parent 2f2fbf4 commit 65c6ec7

File tree

7 files changed

+171
-85
lines changed

7 files changed

+171
-85
lines changed

docs/reference/generated/slider-root.json

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
"default": "'horizontal'",
7373
"description": "The component orientation."
7474
},
75+
"inputRef": {
76+
"type": "Ref<HTMLInputElement>",
77+
"description": "A ref to access the hidden input element."
78+
},
7579
"className": {
7680
"type": "string | ((state: Slider.Root.State) => string)",
7781
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."

docs/reference/generated/slider-thumb.json

-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515
"default": "false",
1616
"description": "Whether the thumb should ignore user interaction."
1717
},
18-
"inputRef": {
19-
"type": "Ref<HTMLInputElement>",
20-
"description": "A ref to access the hidden input element."
21-
},
2218
"className": {
2319
"type": "string | ((state: Slider.Thumb.State) => string)",
2420
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Field } from '@base-ui-components/react/field';
4+
import { Form } from '@base-ui-components/react/form';
5+
import { Slider } from '@base-ui-components/react/slider';
6+
import formStyles from '../../../(public)/(content)/react/components/form/demos/hero/css-modules/index.module.css';
7+
import styles from './slider.module.css';
8+
9+
interface FormValues {
10+
priceRange: string;
11+
}
12+
13+
export default function ExampleForm() {
14+
const [errors, setErrors] = React.useState({});
15+
const [loading, setLoading] = React.useState(false);
16+
17+
return (
18+
<Form
19+
className={formStyles.Form}
20+
style={{ maxWidth: '18rem' }}
21+
errors={errors}
22+
onClearErrors={setErrors}
23+
onSubmit={async (event) => {
24+
event.preventDefault();
25+
const formData = new FormData(event.currentTarget);
26+
const formValues = Object.fromEntries(formData as any) as FormValues;
27+
console.log(formValues);
28+
29+
setLoading(true);
30+
const response = await submitForm(formValues);
31+
const serverErrors = {
32+
priceRange: response.errors?.priceRange,
33+
};
34+
35+
setErrors(serverErrors);
36+
setLoading(false);
37+
console.log('response', response);
38+
}}
39+
>
40+
<Field.Root name="priceRange" className={formStyles.Field}>
41+
<Slider.Root
42+
defaultValue={[500, 1200]}
43+
min={100}
44+
max={2000}
45+
step={1}
46+
minStepsBetweenValues={1}
47+
className={styles.Root}
48+
format={{
49+
style: 'currency',
50+
currency: 'EUR',
51+
}}
52+
locale="nl-NL"
53+
style={{ width: '18rem' }}
54+
>
55+
<Field.Label className={styles.Label}>Price range</Field.Label>
56+
<Slider.Value className={styles.Value} />
57+
<Slider.Control className={styles.Control}>
58+
<Slider.Track className={styles.Track}>
59+
<Slider.Indicator className={styles.Indicator} />
60+
<Slider.Thumb className={styles.Thumb} />
61+
<Slider.Thumb className={styles.Thumb} />
62+
</Slider.Track>
63+
</Slider.Control>
64+
</Slider.Root>
65+
<Field.Error className={formStyles.Error} />
66+
</Field.Root>
67+
<button disabled={loading} type="submit" className={formStyles.Button}>
68+
Search
69+
</button>
70+
</Form>
71+
);
72+
}
73+
74+
async function submitForm(formValues: FormValues) {
75+
await new Promise((resolve) => {
76+
setTimeout(resolve, 600);
77+
});
78+
79+
try {
80+
if (formValues.priceRange === '') {
81+
return { errors: { priceRange: 'server error: empty string' } };
82+
}
83+
} catch {
84+
return { errors: { priceRange: 'server error' } };
85+
}
86+
87+
return {
88+
response: {
89+
results: Math.floor(Math.random() * 20) + 1,
90+
},
91+
};
92+
}

packages/react/src/slider/control/SliderControl.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export const SliderControl = React.forwardRef(function SliderControl(
105105
onValueCommitted,
106106
orientation,
107107
range,
108-
registerInputValidationRef,
109108
setActive,
110109
setDragging,
111110
setValue,
@@ -353,7 +352,7 @@ export const SliderControl = React.forwardRef(function SliderControl(
353352

354353
const renderElement = useRenderElement('div', componentProps, {
355354
state,
356-
ref: [forwardedRef, registerInputValidationRef, controlRef, setStylesRef],
355+
ref: [forwardedRef, controlRef, setStylesRef],
357356
props: [
358357
{
359358
onPointerDown(event: React.PointerEvent<HTMLDivElement>) {

packages/react/src/slider/root/SliderRoot.tsx

+32-16
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,24 @@ import type { BaseUIComponentProps, Orientation } from '../../utils/types';
88
import { useBaseUiId } from '../../utils/useBaseUiId';
99
import { useControlled } from '../../utils/useControlled';
1010
import { useEventCallback } from '../../utils/useEventCallback';
11+
import { useForkRef } from '../../utils/useForkRef';
1112
import { useLatestRef } from '../../utils/useLatestRef';
1213
import { useModernLayoutEffect } from '../../utils/useModernLayoutEffect';
1314
import { useRenderElement } from '../../utils/useRenderElement';
15+
import { visuallyHidden } from '../../utils/visuallyHidden';
1416
import { warn } from '../../utils/warn';
1517
import { CompositeList, type CompositeMetadata } from '../../composite/list/CompositeList';
1618
import type { FieldRoot } from '../../field/root/FieldRoot';
1719
import { useField } from '../../field/useField';
1820
import { useFieldControlValidation } from '../../field/control/useFieldControlValidation';
1921
import { useFieldRootContext } from '../../field/root/FieldRootContext';
22+
import { useFormContext } from '../../form/FormContext';
2023
import { asc } from '../utils/asc';
2124
import { getSliderValue } from '../utils/getSliderValue';
2225
import { validateMinimumDistance } from '../utils/validateMinimumDistance';
2326
import type { ThumbMetadata } from '../thumb/SliderThumb';
2427
import { sliderStyleHookMapping } from './styleHooks';
2528
import { SliderRootContext } from './SliderRootContext';
26-
import { useFormContext } from '../../form/FormContext';
2729

2830
function areValuesEqual(
2931
newValue: number | readonly number[],
@@ -53,6 +55,7 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
5355
defaultValue,
5456
disabled: disabledProp = false,
5557
id: idProp,
58+
inputRef: inputRefProp,
5659
format,
5760
largeStep = 10,
5861
locale,
@@ -93,6 +96,7 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
9396

9497
const {
9598
getValidationProps,
99+
getInputValidationProps,
96100
inputRef: inputValidationRef,
97101
commitValidation,
98102
} = useFieldControlValidation();
@@ -110,10 +114,12 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
110114
});
111115

112116
const sliderRef = React.useRef<HTMLElement>(null);
113-
const controlRef: React.RefObject<HTMLElement | null> = React.useRef(null);
117+
const controlRef = React.useRef<HTMLElement>(null);
118+
const hiddenInputRef = React.useRef<HTMLInputElement>(null);
114119
const thumbRefs = React.useRef<(HTMLElement | null)[]>([]);
115120
const lastChangedValueRef = React.useRef<number | readonly number[] | null>(null);
116121
const formatOptionsRef = useLatestRef(format);
122+
const mergedInputRef = useForkRef(inputRefProp, hiddenInputRef, inputValidationRef);
117123

118124
// We can't use the :active browser pseudo-classes.
119125
// - The active state isn't triggered when clicking on the rail.
@@ -138,16 +144,6 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
138144
controlRef,
139145
});
140146

141-
const registerInputValidationRef = React.useCallback(
142-
(element: HTMLElement | null) => {
143-
if (element) {
144-
controlRef.current = element;
145-
inputValidationRef.current = element.querySelector<HTMLInputElement>('input[type="range"]');
146-
}
147-
},
148-
[inputValidationRef],
149-
);
150-
151147
const range = Array.isArray(valueUnwrapped);
152148

153149
const values = React.useMemo(() => {
@@ -272,11 +268,9 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
272268
max,
273269
min,
274270
minStepsBetweenValues,
275-
name,
276271
onValueCommitted,
277272
orientation,
278273
range,
279-
registerInputValidationRef,
280274
setActive,
281275
setDragging,
282276
setValue,
@@ -301,11 +295,9 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
301295
max,
302296
min,
303297
minStepsBetweenValues,
304-
name,
305298
onValueCommitted,
306299
orientation,
307300
range,
308-
registerInputValidationRef,
309301
setActive,
310302
setDragging,
311303
setValue,
@@ -317,6 +309,16 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
317309
],
318310
);
319311

312+
const serializedValue = React.useMemo(() => {
313+
if (valueUnwrapped == null) {
314+
return ''; // avoid uncontrolled -> controlled error
315+
}
316+
if (!Array.isArray(valueUnwrapped)) {
317+
return valueUnwrapped;
318+
}
319+
return JSON.stringify(valueUnwrapped);
320+
}, [valueUnwrapped]);
321+
320322
const renderElement = useRenderElement('div', componentProps, {
321323
state,
322324
ref: [forwardedRef, sliderRef],
@@ -336,6 +338,16 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
336338
<SliderRootContext.Provider value={contextValue}>
337339
<CompositeList elementsRef={thumbRefs} onMapChange={setThumbMap}>
338340
{renderElement()}
341+
<input
342+
type="hidden"
343+
{...getInputValidationProps({
344+
disabled,
345+
name,
346+
ref: mergedInputRef,
347+
value: serializedValue,
348+
style: visuallyHidden,
349+
})}
350+
/>
339351
</CompositeList>
340352
</SliderRootContext.Provider>
341353
);
@@ -401,6 +413,10 @@ export namespace SliderRoot {
401413
* Options to format the input value.
402414
*/
403415
format?: Intl.NumberFormatOptions;
416+
/**
417+
* A ref to access the hidden input element.
418+
*/
419+
inputRef?: React.Ref<HTMLInputElement>;
404420
/**
405421
* The locale used by `Intl.NumberFormat` when formatting the value.
406422
* Defaults to the user's runtime locale.

packages/react/src/slider/root/SliderRootContext.ts

-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export interface SliderRootContext {
4343
* The minimum steps between values in a range slider.
4444
*/
4545
minStepsBetweenValues: number;
46-
name: string;
4746
/**
4847
* Function to be called when drag ends and the pointer is released.
4948
*/
@@ -57,7 +56,6 @@ export interface SliderRootContext {
5756
* Whether the slider is a range slider.
5857
*/
5958
range: boolean;
60-
registerInputValidationRef: (element: HTMLElement | null) => void;
6159
setActive: React.Dispatch<React.SetStateAction<number>>;
6260
setDragging: React.Dispatch<React.SetStateAction<boolean>>;
6361
/**

0 commit comments

Comments
 (0)