Skip to content
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

Forward form attribute to all form control elements #3161

Merged
merged 2 commits into from
Oct 1, 2024
Merged
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
9 changes: 9 additions & 0 deletions .yarn/versions/2adbb5fd.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion packages/react/checkbox/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ const Checkbox = React.forwardRef<CheckboxElement, CheckboxProps>(
disabled,
value = 'on',
onCheckedChange,
form,
...checkboxProps
} = props;
const [button, setButton] = React.useState<HTMLButtonElement | null>(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,
Expand Down Expand Up @@ -108,6 +109,7 @@ const Checkbox = React.forwardRef<CheckboxElement, CheckboxProps>(
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.
Expand Down
4 changes: 3 additions & 1 deletion packages/react/radio-group/src/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ const Radio = React.forwardRef<RadioElement, RadioProps>(
disabled,
value = 'on',
onCheck,
form,
...radioProps
} = props;
const [button, setButton] = React.useState<HTMLButtonElement | null>(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 (
<RadioProvider scope={__scopeRadio} checked={checked} disabled={disabled}>
Expand Down Expand Up @@ -80,6 +81,7 @@ const Radio = React.forwardRef<RadioElement, RadioProps>(
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.
Expand Down
2 changes: 1 addition & 1 deletion packages/react/select/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const Select: React.FC<SelectProps> = (props: ScopedProps<SelectProps>) => {
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<NativeOption>());

// The native `select` only associates the correct default value if the corresponding
Expand Down
11 changes: 8 additions & 3 deletions packages/react/slider/src/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;
thumbs: Set<SliderThumbElement>;
orientation: SliderProps['orientation'];
form: string | undefined;
};

const [SliderProvider, useSliderContext] = createSliderContext<SliderContextValue>(SLIDER_NAME);
Expand All @@ -71,6 +72,7 @@ interface SliderProps
onValueChange?(value: number[]): void;
onValueCommit?(value: number[]): void;
inverted?: boolean;
form?: string;
}

const Slider = React.forwardRef<SliderElement, SliderProps>(
Expand All @@ -88,6 +90,7 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
onValueChange = () => {},
onValueCommit = () => {},
inverted = false,
form,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A slider can render multiple thumbs with separate inputs. Form attribute should be shared, so lifting this one to the root.

...sliderProps
} = props;
const thumbRefs = React.useRef<SliderContextValue['thumbs']>(new Set());
Expand Down Expand Up @@ -151,6 +154,7 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
thumbs={thumbRefs.current}
values={values}
orientation={orientation}
form={form}
>
<Collection.Provider scope={props.__scopeSlider}>
<Collection.Slot scope={props.__scopeSlider}>
Expand Down Expand Up @@ -556,7 +560,7 @@ const SliderThumbImpl = React.forwardRef<SliderThumbImplElement, SliderThumbImpl
const [thumb, setThumb] = React.useState<HTMLSpanElement | null>(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;
Expand Down Expand Up @@ -618,6 +622,7 @@ const SliderThumbImpl = React.forwardRef<SliderThumbImplElement, SliderThumbImpl
name ??
(context.name ? context.name + (context.values.length > 1 ? '[]' : '') : undefined)
}
form={context.form}
value={value}
/>
)}
Expand Down
4 changes: 3 additions & 1 deletion packages/react/switch/src/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ const Switch = React.forwardRef<SwitchElement, SwitchProps>(
disabled,
value = 'on',
onCheckedChange,
form,
...switchProps
} = props;
const [button, setButton] = React.useState<HTMLButtonElement | null>(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,
Expand Down Expand Up @@ -87,6 +88,7 @@ const Switch = React.forwardRef<SwitchElement, SwitchProps>(
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.
Expand Down
Loading