diff --git a/docs/reference/generated/radio-group.json b/docs/reference/generated/radio-group.json index 23fd13b5ed..9d0dfd9c63 100644 --- a/docs/reference/generated/radio-group.json +++ b/docs/reference/generated/radio-group.json @@ -33,6 +33,10 @@ "default": "false", "description": "Whether the user must choose a value before submitting a form." }, + "inputRef": { + "type": "React.Ref", + "description": "The ref to the hidden input element." + }, "className": { "type": "string | (state) => string", "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." diff --git a/docs/reference/generated/radio-root.json b/docs/reference/generated/radio-root.json index 802113f470..fab6dee002 100644 --- a/docs/reference/generated/radio-root.json +++ b/docs/reference/generated/radio-root.json @@ -22,6 +22,10 @@ "default": "false", "description": "Whether the user must choose a value before submitting a form." }, + "inputRef": { + "type": "React.Ref", + "description": "The ref to the hidden input element." + }, "className": { "type": "string | (state) => string", "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." diff --git a/docs/reference/generated/select-root.json b/docs/reference/generated/select-root.json index 9f4ea89248..a23c13f336 100644 --- a/docs/reference/generated/select-root.json +++ b/docs/reference/generated/select-root.json @@ -64,6 +64,10 @@ "type": "boolean", "default": "false", "description": "Whether the user must choose a value before submitting a form." + }, + "inputRef": { + "type": "React.Ref", + "description": "The ref to the hidden input element." } }, "dataAttributes": {}, diff --git a/packages/react/src/radio-group/RadioGroup.tsx b/packages/react/src/radio-group/RadioGroup.tsx index fb68ce6b5e..e83fa32caa 100644 --- a/packages/react/src/radio-group/RadioGroup.tsx +++ b/packages/react/src/radio-group/RadioGroup.tsx @@ -33,6 +33,7 @@ const RadioGroup = React.forwardRef(function RadioGroup( required, onValueChange: onValueChangeProp, name, + inputRef, ...otherProps } = props; @@ -136,6 +137,10 @@ namespace RadioGroup { * Callback fired when the value changes. */ onValueChange?: (value: unknown, event: Event) => void; + /** + * The ref to the hidden input element. + */ + inputRef?: React.Ref; } } @@ -164,6 +169,15 @@ RadioGroup.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, + /** + * The ref to the hidden input element. + */ + inputRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + current: PropTypes.object, + }), + ]), /** * Identifies the field when a form is submitted. */ diff --git a/packages/react/src/radio-group/useRadioGroup.ts b/packages/react/src/radio-group/useRadioGroup.ts index 5c6ba9de95..4173792784 100644 --- a/packages/react/src/radio-group/useRadioGroup.ts +++ b/packages/react/src/radio-group/useRadioGroup.ts @@ -7,7 +7,7 @@ import { useFieldRootContext } from '../field/root/FieldRootContext'; import { useBaseUiId } from '../utils/useBaseUiId'; import { useFieldControlValidation } from '../field/control/useFieldControlValidation'; import { useField } from '../field/useField'; -import { visuallyHidden } from '../utils'; +import { useForkRef, visuallyHidden } from '../utils'; export function useRadioGroup(params: useRadioGroup.Parameters) { const { @@ -17,6 +17,7 @@ export function useRadioGroup(params: useRadioGroup.Parameters) { defaultValue, readOnly, value: externalValue, + inputRef: inputRefProp, } = params; const { @@ -35,6 +36,8 @@ export function useRadioGroup(params: useRadioGroup.Parameters) { const id = useBaseUiId(); + const ref = useForkRef(fieldControlValidation.inputRef, inputRefProp); + const [checkedValue, setCheckedValue] = useControlled({ controlled: externalValue, default: defaultValue, @@ -109,7 +112,7 @@ export function useRadioGroup(params: useRadioGroup.Parameters) { mergeProps<'input'>( { value: serializedCheckedValue, - ref: fieldControlValidation.inputRef, + ref, id, name, disabled, @@ -121,7 +124,7 @@ export function useRadioGroup(params: useRadioGroup.Parameters) { }, fieldControlValidation.getInputValidationProps(externalProps), ), - [fieldControlValidation, serializedCheckedValue, id, name, disabled, readOnly, required], + [serializedCheckedValue, ref, id, name, disabled, readOnly, required, fieldControlValidation], ); return React.useMemo( @@ -146,5 +149,6 @@ namespace useRadioGroup { readOnly?: boolean; defaultValue?: unknown; value?: unknown; + inputRef?: React.Ref; } } diff --git a/packages/react/src/radio/root/RadioRoot.tsx b/packages/react/src/radio/root/RadioRoot.tsx index 6f051ebe3a..fbd896db34 100644 --- a/packages/react/src/radio/root/RadioRoot.tsx +++ b/packages/react/src/radio/root/RadioRoot.tsx @@ -27,6 +27,7 @@ const RadioRoot = React.forwardRef(function RadioRoot( disabled: disabledProp = false, readOnly: readOnlyProp = false, required: requiredProp = false, + inputRef, ...otherProps } = props; @@ -47,6 +48,7 @@ const RadioRoot = React.forwardRef(function RadioRoot( ...props, disabled, readOnly, + inputRef, }); const state: RadioRoot.State = React.useMemo( @@ -101,6 +103,10 @@ namespace RadioRoot { * @default false */ readOnly?: boolean; + /** + * The ref to the hidden input element. + */ + inputRef?: React.Ref; } export interface State { @@ -139,6 +145,15 @@ RadioRoot.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, + /** + * The ref to the hidden input element. + */ + inputRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + current: PropTypes.object, + }), + ]), /** * Whether the user should be unable to select the radio button. * @default false diff --git a/packages/react/src/radio/root/useRadioRoot.tsx b/packages/react/src/radio/root/useRadioRoot.tsx index 24542d20d9..cde453e32a 100644 --- a/packages/react/src/radio/root/useRadioRoot.tsx +++ b/packages/react/src/radio/root/useRadioRoot.tsx @@ -5,9 +5,10 @@ import { visuallyHidden } from '../../utils/visuallyHidden'; import { useRadioGroupContext } from '../../radio-group/RadioGroupContext'; import { useFieldRootContext } from '../../field/root/FieldRootContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useForkRef } from '../../utils'; export function useRadioRoot(params: useRadioRoot.Parameters) { - const { disabled, readOnly, value, required } = params; + const { disabled, readOnly, value, required, inputRef: inputRefProp } = params; const { checkedValue, @@ -29,6 +30,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { const checked = checkedValue === value; const inputRef = React.useRef(null); + const ref = useForkRef(inputRefProp, inputRef); useEnhancedEffect(() => { if (inputRef.current?.checked) { @@ -83,7 +85,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { mergeProps<'input'>( { type: 'radio', - ref: inputRef, + ref, tabIndex: -1, style: visuallyHidden, 'aria-hidden': true, @@ -115,6 +117,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) { externalProps, ), [ + ref, disabled, checked, required, @@ -147,6 +150,7 @@ namespace useRadioRoot { disabled?: boolean; readOnly?: boolean; required?: boolean; + inputRef?: React.Ref; } export interface ReturnValue { diff --git a/packages/react/src/select/root/SelectRoot.tsx b/packages/react/src/select/root/SelectRoot.tsx index 4f0201167b..48e5e96c08 100644 --- a/packages/react/src/select/root/SelectRoot.tsx +++ b/packages/react/src/select/root/SelectRoot.tsx @@ -6,6 +6,7 @@ import { SelectRootContext } from './SelectRootContext'; import { SelectIndexContext } from './SelectIndexContext'; import { useFieldRootContext } from '../../field/root/FieldRootContext'; import { visuallyHidden } from '../../utils/visuallyHidden'; +import { useForkRef } from '../../utils'; /** * Groups all parts of the select. @@ -30,6 +31,7 @@ const SelectRoot: SelectRoot = function SelectRoot( required = false, modal = true, actionsRef, + inputRef, onOpenChangeComplete, } = props; @@ -55,6 +57,8 @@ const SelectRoot: SelectRoot = function SelectRoot( const { rootContext } = selectRoot; const value = rootContext.value; + const ref = useForkRef(inputRef, rootContext.fieldControlValidation.inputRef); + const serializedValue = React.useMemo(() => { if (value == null) { return ''; // avoid uncontrolled -> controlled error @@ -105,7 +109,7 @@ const SelectRoot: SelectRoot = function SelectRoot( required: rootContext.required, readOnly: rootContext.readOnly, value: serializedValue, - ref: rootContext.fieldControlValidation.inputRef, + ref, style: visuallyHidden, tabIndex: -1, 'aria-hidden': true, @@ -119,6 +123,10 @@ const SelectRoot: SelectRoot = function SelectRoot( namespace SelectRoot { export interface Props extends useSelectRoot.Parameters { children?: React.ReactNode; + /** + * The ref to the hidden input element. + */ + inputRef?: React.Ref; } export interface State {} @@ -172,6 +180,15 @@ SelectRoot.propTypes /* remove-proptypes */ = { * @default false */ disabled: PropTypes.bool, + /** + * The ref to the hidden input element. + */ + inputRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + current: PropTypes.object, + }), + ]), /** * Whether the select should prevent outside clicks and lock page scroll when open. * @default true diff --git a/packages/react/src/slider/root/SliderRoot.tsx b/packages/react/src/slider/root/SliderRoot.tsx index 6041efae68..e3ecbba113 100644 --- a/packages/react/src/slider/root/SliderRoot.tsx +++ b/packages/react/src/slider/root/SliderRoot.tsx @@ -102,7 +102,7 @@ const SliderRoot = React.forwardRef(function SliderRoot ({ ...slider, format, @@ -236,6 +236,7 @@ namespace SliderRoot { onValueCommitted?: (value: Value extends number ? number : Value, event: Event) => void; } } + export { SliderRoot }; SliderRoot.propTypes /* remove-proptypes */ = { diff --git a/packages/react/src/slider/thumb/SliderThumb.tsx b/packages/react/src/slider/thumb/SliderThumb.tsx index 1ca824aa8c..e75f238c08 100644 --- a/packages/react/src/slider/thumb/SliderThumb.tsx +++ b/packages/react/src/slider/thumb/SliderThumb.tsx @@ -50,6 +50,7 @@ const SliderThumb = React.forwardRef(function SliderThumb( onFocus: onFocusProp, onKeyDown: onKeyDownProp, tabIndex: tabIndexProp, + inputRef, ...otherProps } = props; @@ -110,6 +111,7 @@ const SliderThumb = React.forwardRef(function SliderThumb( step, tabIndex: tabIndexProp ?? contextTabIndex, values, + inputRef, }); const styleHooks = React.useMemo( @@ -233,6 +235,15 @@ SliderThumb.propTypes /* remove-proptypes */ = { * @ignore */ inputId: PropTypes.string, + /** + * @ignore + */ + inputRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + current: PropTypes.object, + }), + ]), /** * @ignore */ diff --git a/packages/react/src/slider/thumb/useSliderThumb.ts b/packages/react/src/slider/thumb/useSliderThumb.ts index 5515760261..49d317e50e 100644 --- a/packages/react/src/slider/thumb/useSliderThumb.ts +++ b/packages/react/src/slider/thumb/useSliderThumb.ts @@ -76,6 +76,7 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider orientation, percentageValues, rootRef: externalRef, + inputRef: inputRefProp, step, tabIndex: externalTabIndex, values: sliderValues, @@ -91,7 +92,7 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider const thumbRef = React.useRef(null); const inputRef = React.useRef(null); - const mergedInputRef = useForkRef(inputRef, inputValidationRef); + const mergedInputRef = useForkRef(inputRef, inputValidationRef, inputRefProp); const thumbMetadata = React.useMemo( () => ({ @@ -403,6 +404,7 @@ export namespace useSliderThumb { * @default null */ tabIndex: number | null; + inputRef: React.Ref | undefined; } export interface ReturnValue { diff --git a/packages/react/src/slider/value/SliderValue.test.tsx b/packages/react/src/slider/value/SliderValue.test.tsx index 6fbc744a3c..896e4dd30c 100644 --- a/packages/react/src/slider/value/SliderValue.test.tsx +++ b/packages/react/src/slider/value/SliderValue.test.tsx @@ -52,6 +52,7 @@ const testRootContext: SliderRootContext = { tabIndex: null, thumbRefs: { current: [] }, values: [0], + inputRef: { current: null }, }; describe('', () => {