From dcd7a357f4e07f4d370c75b9c84603a4237f7cc2 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 1 Oct 2024 13:38:54 -0700 Subject: [PATCH] Forward `form` attribute to all form control elements (#3161) --- .yarn/versions/2adbb5fd.yml | 9 +++++++++ packages/react/checkbox/src/Checkbox.tsx | 4 +++- packages/react/radio-group/src/Radio.tsx | 4 +++- packages/react/select/src/Select.tsx | 2 +- packages/react/slider/src/Slider.tsx | 11 ++++++++--- packages/react/switch/src/Switch.tsx | 4 +++- 6 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 .yarn/versions/2adbb5fd.yml diff --git a/.yarn/versions/2adbb5fd.yml b/.yarn/versions/2adbb5fd.yml new file mode 100644 index 0000000000..ee0fc06861 --- /dev/null +++ b/.yarn/versions/2adbb5fd.yml @@ -0,0 +1,9 @@ +releases: + "@radix-ui/react-checkbox": patch + "@radix-ui/react-radio-group": patch + "@radix-ui/react-select": patch + "@radix-ui/react-slider": patch + "@radix-ui/react-switch": patch + +declined: + - primitives diff --git a/packages/react/checkbox/src/Checkbox.tsx b/packages/react/checkbox/src/Checkbox.tsx index 65fc242dc2..95fef9995a 100644 --- a/packages/react/checkbox/src/Checkbox.tsx +++ b/packages/react/checkbox/src/Checkbox.tsx @@ -49,13 +49,14 @@ const Checkbox = React.forwardRef( disabled, value = 'on', onCheckedChange, + form, ...checkboxProps } = props; const [button, setButton] = React.useState(null); const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node)); const hasConsumerStoppedPropagationRef = React.useRef(false); // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = button ? Boolean(button.closest('form')) : true; + const isFormControl = button ? form || !!button.closest('form') : true; const [checked = false, setChecked] = useControllableState({ prop: checkedProp, defaultProp: defaultChecked, @@ -108,6 +109,7 @@ const Checkbox = React.forwardRef( checked={checked} required={required} disabled={disabled} + form={form} // We transform because the input is absolutely positioned but we have // rendered it **after** the button. This pulls it back to sit on top // of the button. diff --git a/packages/react/radio-group/src/Radio.tsx b/packages/react/radio-group/src/Radio.tsx index 5fdb7fcd4d..45911b28ce 100644 --- a/packages/react/radio-group/src/Radio.tsx +++ b/packages/react/radio-group/src/Radio.tsx @@ -39,13 +39,14 @@ const Radio = React.forwardRef( disabled, value = 'on', onCheck, + form, ...radioProps } = props; const [button, setButton] = React.useState(null); const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node)); const hasConsumerStoppedPropagationRef = React.useRef(false); // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = button ? Boolean(button.closest('form')) : true; + const isFormControl = button ? form || !!button.closest('form') : true; return ( @@ -80,6 +81,7 @@ const Radio = React.forwardRef( checked={checked} required={required} disabled={disabled} + form={form} // We transform because the input is absolutely positioned but we have // rendered it **after** the button. This pulls it back to sit on top // of the button. diff --git a/packages/react/select/src/Select.tsx b/packages/react/select/src/Select.tsx index 9ad418f091..5e848d8caf 100644 --- a/packages/react/select/src/Select.tsx +++ b/packages/react/select/src/Select.tsx @@ -129,7 +129,7 @@ const Select: React.FC = (props: ScopedProps) => { const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null); // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = trigger ? Boolean(trigger.closest('form')) || form : true; + const isFormControl = trigger ? form || !!trigger.closest('form') : true; const [nativeOptionsSet, setNativeOptionsSet] = React.useState(new Set()); // The native `select` only associates the correct default value if the corresponding diff --git a/packages/react/slider/src/Slider.tsx b/packages/react/slider/src/Slider.tsx index 00288e74ae..a064090e7f 100644 --- a/packages/react/slider/src/Slider.tsx +++ b/packages/react/slider/src/Slider.tsx @@ -40,14 +40,15 @@ const [createSliderContext, createSliderScope] = createContextScope(SLIDER_NAME, ]); type SliderContextValue = { - name?: string; - disabled?: boolean; + name: string | undefined; + disabled: boolean | undefined; min: number; max: number; values: number[]; valueIndexToChangeRef: React.MutableRefObject; thumbs: Set; orientation: SliderProps['orientation']; + form: string | undefined; }; const [SliderProvider, useSliderContext] = createSliderContext(SLIDER_NAME); @@ -71,6 +72,7 @@ interface SliderProps onValueChange?(value: number[]): void; onValueCommit?(value: number[]): void; inverted?: boolean; + form?: string; } const Slider = React.forwardRef( @@ -88,6 +90,7 @@ const Slider = React.forwardRef( onValueChange = () => {}, onValueCommit = () => {}, inverted = false, + form, ...sliderProps } = props; const thumbRefs = React.useRef(new Set()); @@ -151,6 +154,7 @@ const Slider = React.forwardRef( thumbs={thumbRefs.current} values={values} orientation={orientation} + form={form} > @@ -556,7 +560,7 @@ const SliderThumbImpl = React.forwardRef(null); const composedRefs = useComposedRefs(forwardedRef, (node) => setThumb(node)); // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = thumb ? Boolean(thumb.closest('form')) : true; + const isFormControl = thumb ? context.form || !!thumb.closest('form') : true; const size = useSize(thumb); // We cast because index could be `-1` which would return undefined const value = context.values[index] as number | undefined; @@ -618,6 +622,7 @@ const SliderThumbImpl = React.forwardRef 1 ? '[]' : '') : undefined) } + form={context.form} value={value} /> )} diff --git a/packages/react/switch/src/Switch.tsx b/packages/react/switch/src/Switch.tsx index e780df512a..96ab46753c 100644 --- a/packages/react/switch/src/Switch.tsx +++ b/packages/react/switch/src/Switch.tsx @@ -41,13 +41,14 @@ const Switch = React.forwardRef( disabled, value = 'on', onCheckedChange, + form, ...switchProps } = props; const [button, setButton] = React.useState(null); const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node)); const hasConsumerStoppedPropagationRef = React.useRef(false); // We set this to true by default so that events bubble to forms without JS (SSR) - const isFormControl = button ? Boolean(button.closest('form')) : true; + const isFormControl = button ? form || !!button.closest('form') : true; const [checked = false, setChecked] = useControllableState({ prop: checkedProp, defaultProp: defaultChecked, @@ -87,6 +88,7 @@ const Switch = React.forwardRef( checked={checked} required={required} disabled={disabled} + form={form} // We transform because the input is absolutely positioned but we have // rendered it **after** the button. This pulls it back to sit on top // of the button.