Skip to content

[form controls] Add inputRef prop to roots #1683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/reference/generated/radio-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/generated/radio-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/generated/select-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/radio-group/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const RadioGroup = React.forwardRef(function RadioGroup(
required,
onValueChange: onValueChangeProp,
name,
inputRef,
...otherProps
} = props;

Expand Down Expand Up @@ -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<HTMLInputElement>;
}
}

Expand Down Expand Up @@ -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.
*/
Expand Down
10 changes: 7 additions & 3 deletions packages/react/src/radio-group/useRadioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,6 +17,7 @@ export function useRadioGroup(params: useRadioGroup.Parameters) {
defaultValue,
readOnly,
value: externalValue,
inputRef: inputRefProp,
} = params;

const {
Expand All @@ -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,
Expand Down Expand Up @@ -109,7 +112,7 @@ export function useRadioGroup(params: useRadioGroup.Parameters) {
mergeProps<'input'>(
{
value: serializedCheckedValue,
ref: fieldControlValidation.inputRef,
ref,
id,
name,
disabled,
Expand All @@ -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(
Expand All @@ -146,5 +149,6 @@ namespace useRadioGroup {
readOnly?: boolean;
defaultValue?: unknown;
value?: unknown;
inputRef?: React.Ref<HTMLInputElement>;
}
}
15 changes: 15 additions & 0 deletions packages/react/src/radio/root/RadioRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const RadioRoot = React.forwardRef(function RadioRoot(
disabled: disabledProp = false,
readOnly: readOnlyProp = false,
required: requiredProp = false,
inputRef,
...otherProps
} = props;

Expand All @@ -47,6 +48,7 @@ const RadioRoot = React.forwardRef(function RadioRoot(
...props,
disabled,
readOnly,
inputRef,
});

const state: RadioRoot.State = React.useMemo(
Expand Down Expand Up @@ -101,6 +103,10 @@ namespace RadioRoot {
* @default false
*/
readOnly?: boolean;
/**
* The ref to the hidden input element.
*/
inputRef?: React.Ref<HTMLInputElement>;
}

export interface State {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/radio/root/useRadioRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +30,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) {
const checked = checkedValue === value;

const inputRef = React.useRef<HTMLInputElement>(null);
const ref = useForkRef(inputRefProp, inputRef);

useEnhancedEffect(() => {
if (inputRef.current?.checked) {
Expand Down Expand Up @@ -83,7 +85,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) {
mergeProps<'input'>(
{
type: 'radio',
ref: inputRef,
ref,
tabIndex: -1,
style: visuallyHidden,
'aria-hidden': true,
Expand Down Expand Up @@ -115,6 +117,7 @@ export function useRadioRoot(params: useRadioRoot.Parameters) {
externalProps,
),
[
ref,
disabled,
checked,
required,
Expand Down Expand Up @@ -147,6 +150,7 @@ namespace useRadioRoot {
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
inputRef?: React.Ref<HTMLInputElement>;
}

export interface ReturnValue {
Expand Down
19 changes: 18 additions & 1 deletion packages/react/src/select/root/SelectRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,6 +31,7 @@ const SelectRoot: SelectRoot = function SelectRoot<Value>(
required = false,
modal = true,
actionsRef,
inputRef,
onOpenChangeComplete,
} = props;

Expand All @@ -55,6 +57,8 @@ const SelectRoot: SelectRoot = function SelectRoot<Value>(
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
Expand Down Expand Up @@ -105,7 +109,7 @@ const SelectRoot: SelectRoot = function SelectRoot<Value>(
required: rootContext.required,
readOnly: rootContext.readOnly,
value: serializedValue,
ref: rootContext.fieldControlValidation.inputRef,
ref,
style: visuallyHidden,
tabIndex: -1,
'aria-hidden': true,
Expand All @@ -119,6 +123,10 @@ const SelectRoot: SelectRoot = function SelectRoot<Value>(
namespace SelectRoot {
export interface Props<Value> extends useSelectRoot.Parameters<Value> {
children?: React.ReactNode;
/**
* The ref to the hidden input element.
*/
inputRef?: React.Ref<HTMLInputElement>;
}

export interface State {}
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/slider/root/SliderRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const SliderRoot = React.forwardRef(function SliderRoot<Value extends number | r
],
);

const contextValue = React.useMemo(
const contextValue: SliderRootContext = React.useMemo(
() => ({
...slider,
format,
Expand Down Expand Up @@ -236,6 +236,7 @@ namespace SliderRoot {
onValueCommitted?: (value: Value extends number ? number : Value, event: Event) => void;
}
}

export { SliderRoot };

SliderRoot.propTypes /* remove-proptypes */ = {
Expand Down
11 changes: 11 additions & 0 deletions packages/react/src/slider/thumb/SliderThumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
onFocus: onFocusProp,
onKeyDown: onKeyDownProp,
tabIndex: tabIndexProp,
inputRef,
...otherProps
} = props;

Expand Down Expand Up @@ -110,6 +111,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
step,
tabIndex: tabIndexProp ?? contextTabIndex,
values,
inputRef,
});

const styleHooks = React.useMemo(
Expand Down Expand Up @@ -233,6 +235,15 @@ SliderThumb.propTypes /* remove-proptypes */ = {
* @ignore
*/
inputId: PropTypes.string,
/**
* @ignore
*/
inputRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
current: PropTypes.object,
}),
]),
/**
* @ignore
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/slider/thumb/useSliderThumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider
orientation,
percentageValues,
rootRef: externalRef,
inputRef: inputRefProp,
step,
tabIndex: externalTabIndex,
values: sliderValues,
Expand All @@ -91,7 +92,7 @@ export function useSliderThumb(parameters: useSliderThumb.Parameters): useSlider

const thumbRef = React.useRef<HTMLElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
const mergedInputRef = useForkRef(inputRef, inputValidationRef);
const mergedInputRef = useForkRef(inputRef, inputValidationRef, inputRefProp);

const thumbMetadata = React.useMemo(
() => ({
Expand Down Expand Up @@ -403,6 +404,7 @@ export namespace useSliderThumb {
* @default null
*/
tabIndex: number | null;
inputRef: React.Ref<HTMLInputElement> | undefined;
}

export interface ReturnValue {
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/slider/value/SliderValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const testRootContext: SliderRootContext = {
tabIndex: null,
thumbRefs: { current: [] },
values: [0],
inputRef: { current: null },
};

describe('<Slider.Value />', () => {
Expand Down