From a19f04d14bf89062adb4dea6a64994d41ab4752d Mon Sep 17 00:00:00 2001 From: Devon Govett <devongovett@gmail.com> Date: Wed, 18 Jun 2025 11:48:41 -0700 Subject: [PATCH 1/2] feat: Support associating components with external forms --- packages/@react-aria/button/src/useButton.ts | 10 ++++- .../checkbox/src/useCheckboxGroup.ts | 3 +- .../checkbox/src/useCheckboxGroupItem.ts | 3 +- packages/@react-aria/checkbox/src/utils.ts | 1 + .../@react-aria/color/src/useColorArea.ts | 5 ++- .../@react-aria/color/src/useColorSlider.ts | 3 +- .../@react-aria/color/src/useColorWheel.ts | 4 +- .../datepicker/src/useDateField.ts | 1 + .../datepicker/src/useDatePicker.ts | 3 +- .../datepicker/src/useDateRangePicker.ts | 2 + .../numberfield/src/useNumberField.ts | 2 + packages/@react-aria/radio/src/useRadio.ts | 3 +- .../@react-aria/radio/src/useRadioGroup.ts | 2 + packages/@react-aria/radio/src/utils.ts | 1 + .../@react-aria/select/src/HiddenSelect.tsx | 13 +++++- packages/@react-aria/select/src/useSelect.ts | 3 ++ .../@react-aria/slider/src/useSliderThumb.ts | 4 +- .../@react-aria/textfield/src/useTextField.ts | 1 + packages/@react-aria/toggle/src/useToggle.ts | 2 + .../numberfield/src/NumberField.tsx | 2 +- .../numberfield/test/NumberField.test.js | 3 +- .../@react-spectrum/picker/src/Picker.tsx | 4 +- .../picker/test/Picker.test.js | 15 +++++++ .../@react-spectrum/s2/src/RangeSlider.tsx | 10 ++++- packages/@react-spectrum/s2/src/Slider.tsx | 2 +- .../slider/src/RangeSlider.tsx | 6 ++- .../@react-spectrum/slider/src/Slider.tsx | 3 +- .../slider/test/RangeSlider.test.tsx | 4 +- .../slider/test/Slider.test.tsx | 3 +- packages/@react-types/button/src/index.d.ts | 25 ++++++++++- packages/@react-types/color/src/index.d.ts | 8 +++- .../@react-types/datepicker/src/index.d.ts | 8 +++- packages/@react-types/select/src/index.d.ts | 8 +++- packages/@react-types/shared/src/dom.d.ts | 8 +++- packages/@react-types/slider/src/index.d.ts | 8 +++- packages/react-aria-components/src/Button.tsx | 26 +----------- .../react-aria-components/src/NumberField.tsx | 2 +- packages/react-aria-components/src/Select.tsx | 1 + .../test/Checkbox.test.js | 6 +++ .../test/CheckboxGroup.test.js | 7 ++++ .../test/ColorArea.test.js | 6 +++ .../test/ColorField.test.js | 9 ++++ .../test/ColorSlider.test.js | 6 +++ .../test/ColorWheel.test.js | 6 +++ .../test/ComboBox.test.js | 6 +++ .../test/DateField.test.js | 3 +- .../test/DatePicker.test.js | 3 +- .../test/DateRangePicker.test.js | 4 +- .../test/NumberField.test.js | 5 ++- .../test/RadioGroup.test.js | 7 ++++ .../test/SearchField.test.js | 9 ++++ .../react-aria-components/test/Select.test.js | 9 ++++ .../react-aria-components/test/Slider.test.js | 41 +++++++++++-------- .../react-aria-components/test/Switch.test.js | 6 +++ .../test/TextField.test.js | 9 ++++ .../test/TimeField.test.js | 3 +- 56 files changed, 281 insertions(+), 76 deletions(-) diff --git a/packages/@react-aria/button/src/useButton.ts b/packages/@react-aria/button/src/useButton.ts index b4fae6cb224..8b33496778c 100644 --- a/packages/@react-aria/button/src/useButton.ts +++ b/packages/@react-aria/button/src/useButton.ts @@ -67,7 +67,15 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject< if (elementType === 'button') { additionalProps = { type, - disabled: isDisabled + disabled: isDisabled, + form: props.form, + formAction: props.formAction, + formEncType: props.formEncType, + formMethod: props.formMethod, + formNoValidate: props.formNoValidate, + formTarget: props.formTarget, + name: props.name, + value: props.value }; } else { additionalProps = { diff --git a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts index d35987aca4c..0056b9978ba 100644 --- a/packages/@react-aria/checkbox/src/useCheckboxGroup.ts +++ b/packages/@react-aria/checkbox/src/useCheckboxGroup.ts @@ -36,7 +36,7 @@ export interface CheckboxGroupAria extends ValidationResult { * @param state - State for the checkbox group, as returned by `useCheckboxGroupState`. */ export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxGroupState): CheckboxGroupAria { - let {isDisabled, name, validationBehavior = 'aria'} = props; + let {isDisabled, name, form, validationBehavior = 'aria'} = props; let {isInvalid, validationErrors, validationDetails} = state.displayValidation; let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({ @@ -50,6 +50,7 @@ export function useCheckboxGroup(props: AriaCheckboxGroupProps, state: CheckboxG checkboxGroupData.set(state, { name, + form, descriptionId: descriptionProps.id, errorMessageId: errorMessageProps.id, validationBehavior diff --git a/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts b/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts index 41a66b79a7d..758abcfb98a 100644 --- a/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts +++ b/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts @@ -43,7 +43,7 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C } }); - let {name, descriptionId, errorMessageId, validationBehavior} = checkboxGroupData.get(state)!; + let {name, form, descriptionId, errorMessageId, validationBehavior} = checkboxGroupData.get(state)!; validationBehavior = props.validationBehavior ?? validationBehavior; // Local validation for this checkbox. @@ -72,6 +72,7 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C isReadOnly: props.isReadOnly || state.isReadOnly, isDisabled: props.isDisabled || state.isDisabled, name: props.name || name, + form: props.form || form, isRequired: props.isRequired ?? state.isRequired, validationBehavior, [privateValidationStateProp]: { diff --git a/packages/@react-aria/checkbox/src/utils.ts b/packages/@react-aria/checkbox/src/utils.ts index 03a13138a63..78bead0bee7 100644 --- a/packages/@react-aria/checkbox/src/utils.ts +++ b/packages/@react-aria/checkbox/src/utils.ts @@ -14,6 +14,7 @@ import {CheckboxGroupState} from '@react-stately/checkbox'; interface CheckboxGroupData { name?: string, + form?: string, descriptionId?: string, errorMessageId?: string, validationBehavior: 'aria' | 'native' diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 2d46099cc7f..16b859fcf31 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -54,7 +54,8 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState) containerRef, 'aria-label': ariaLabel, xName, - yName + yName, + form } = props; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/color'); @@ -431,6 +432,7 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState) disabled: isDisabled, value: state.value.getChannelValue(xChannel), name: xName, + form, tabIndex: (isMobile || !focusedInput || focusedInput === 'x' ? undefined : -1), /* So that only a single "2d slider" control shows up when listing form elements for screen readers, @@ -456,6 +458,7 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState) disabled: isDisabled, value: state.value.getChannelValue(yChannel), name: yName, + form, tabIndex: (isMobile || focusedInput === 'y' ? undefined : -1), /* So that only a single "2d slider" control shows up when listing form elements for screen readers, diff --git a/packages/@react-aria/color/src/useColorSlider.ts b/packages/@react-aria/color/src/useColorSlider.ts index 22e5c50cfc6..804914a983f 100644 --- a/packages/@react-aria/color/src/useColorSlider.ts +++ b/packages/@react-aria/color/src/useColorSlider.ts @@ -44,7 +44,7 @@ export interface ColorSliderAria { * Color sliders allow users to adjust an individual channel of a color value. */ export function useColorSlider(props: AriaColorSliderOptions, state: ColorSliderState): ColorSliderAria { - let {trackRef, inputRef, orientation, channel, 'aria-label': ariaLabel, name} = props; + let {trackRef, inputRef, orientation, channel, 'aria-label': ariaLabel, name, form} = props; let {locale, direction} = useLocale(); @@ -60,6 +60,7 @@ export function useColorSlider(props: AriaColorSliderOptions, state: ColorSlider orientation, isDisabled: props.isDisabled, name, + form, trackRef, inputRef }, state); diff --git a/packages/@react-aria/color/src/useColorWheel.ts b/packages/@react-aria/color/src/useColorWheel.ts index d645f4fb06a..039c464abf0 100644 --- a/packages/@react-aria/color/src/useColorWheel.ts +++ b/packages/@react-aria/color/src/useColorWheel.ts @@ -45,7 +45,8 @@ export function useColorWheel(props: AriaColorWheelOptions, state: ColorWheelSta innerRadius, outerRadius, 'aria-label': ariaLabel, - name + name, + form } = props; let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); @@ -325,6 +326,7 @@ export function useColorWheel(props: AriaColorWheelOptions, state: ColorWheelSta disabled: isDisabled, value: `${state.value.getChannelValue('hue')}`, name, + form, onChange: (e: ChangeEvent<HTMLInputElement>) => { state.setHue(parseFloat(e.target.value)); }, diff --git a/packages/@react-aria/datepicker/src/useDateField.ts b/packages/@react-aria/datepicker/src/useDateField.ts index cf4e64216f4..0fbcbd68885 100644 --- a/packages/@react-aria/datepicker/src/useDateField.ts +++ b/packages/@react-aria/datepicker/src/useDateField.ts @@ -149,6 +149,7 @@ export function useDateField<T extends DateValue>(props: AriaDateFieldOptions<T> let inputProps: InputHTMLAttributes<HTMLInputElement> = { type: 'hidden', name: props.name, + form: props.form, value: state.value?.toString() || '', disabled: props.isDisabled }; diff --git a/packages/@react-aria/datepicker/src/useDatePicker.ts b/packages/@react-aria/datepicker/src/useDatePicker.ts index 70980d1e17e..571c53fd179 100644 --- a/packages/@react-aria/datepicker/src/useDatePicker.ts +++ b/packages/@react-aria/datepicker/src/useDatePicker.ts @@ -149,7 +149,8 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T> // DatePicker owns the validation state for the date field. [privateValidationStateProp]: state, autoFocus: props.autoFocus, - name: props.name + name: props.name, + form: props.form }, descriptionProps, errorMessageProps, diff --git a/packages/@react-aria/datepicker/src/useDateRangePicker.ts b/packages/@react-aria/datepicker/src/useDateRangePicker.ts index 7f80ee33578..1009798371c 100644 --- a/packages/@react-aria/datepicker/src/useDateRangePicker.ts +++ b/packages/@react-aria/datepicker/src/useDateRangePicker.ts @@ -186,6 +186,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick onChange: start => state.setDateTime('start', start), autoFocus: props.autoFocus, name: props.startName, + form: props.form, [privateValidationStateProp]: { realtimeValidation: state.realtimeValidation, displayValidation: state.displayValidation, @@ -203,6 +204,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick value: state.value?.end ?? null, onChange: end => state.setDateTime('end', end), name: props.endName, + form: props.form, [privateValidationStateProp]: { realtimeValidation: state.realtimeValidation, displayValidation: state.displayValidation, diff --git a/packages/@react-aria/numberfield/src/useNumberField.ts b/packages/@react-aria/numberfield/src/useNumberField.ts index 9411cfb9570..b056f140e09 100644 --- a/packages/@react-aria/numberfield/src/useNumberField.ts +++ b/packages/@react-aria/numberfield/src/useNumberField.ts @@ -195,7 +195,9 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt let {labelProps, inputProps: textFieldProps, descriptionProps, errorMessageProps} = useFormattedTextField({ ...otherProps, ...domProps, + // These props are added to a hidden input rather than the formatted textfield. name: undefined, + form: undefined, label, autoFocus, isDisabled, diff --git a/packages/@react-aria/radio/src/useRadio.ts b/packages/@react-aria/radio/src/useRadio.ts index 3c4f0220a22..c2291b16b8a 100644 --- a/packages/@react-aria/radio/src/useRadio.ts +++ b/packages/@react-aria/radio/src/useRadio.ts @@ -93,7 +93,7 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref tabIndex = undefined; } - let {name, descriptionId, errorMessageId, validationBehavior} = radioGroupData.get(state)!; + let {name, form, descriptionId, errorMessageId, validationBehavior} = radioGroupData.get(state)!; useFormReset(ref, state.selectedValue, state.setSelectedValue); useFormValidation({validationBehavior}, state, ref); @@ -103,6 +103,7 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref ...interactions, type: 'radio', name, + form, tabIndex, disabled: isDisabled, required: state.isRequired && validationBehavior === 'native', diff --git a/packages/@react-aria/radio/src/useRadioGroup.ts b/packages/@react-aria/radio/src/useRadioGroup.ts index 5db55e3da64..73a886495bb 100644 --- a/packages/@react-aria/radio/src/useRadioGroup.ts +++ b/packages/@react-aria/radio/src/useRadioGroup.ts @@ -40,6 +40,7 @@ export interface RadioGroupAria extends ValidationResult { export function useRadioGroup(props: AriaRadioGroupProps, state: RadioGroupState): RadioGroupAria { let { name, + form, isReadOnly, isRequired, isDisabled, @@ -126,6 +127,7 @@ export function useRadioGroup(props: AriaRadioGroupProps, state: RadioGroupState let groupName = useId(name); radioGroupData.set(state, { name: groupName, + form, descriptionId: descriptionProps.id, errorMessageId: errorMessageProps.id, validationBehavior diff --git a/packages/@react-aria/radio/src/utils.ts b/packages/@react-aria/radio/src/utils.ts index 01fcfc88a52..0ba6ce2de12 100644 --- a/packages/@react-aria/radio/src/utils.ts +++ b/packages/@react-aria/radio/src/utils.ts @@ -14,6 +14,7 @@ import {RadioGroupState} from '@react-stately/radio'; interface RadioGroupData { name: string, + form: string | undefined, descriptionId: string | undefined, errorMessageId: string | undefined, validationBehavior: 'aria' | 'native' diff --git a/packages/@react-aria/select/src/HiddenSelect.tsx b/packages/@react-aria/select/src/HiddenSelect.tsx index b0d61ddc2aa..1875b5b3ebc 100644 --- a/packages/@react-aria/select/src/HiddenSelect.tsx +++ b/packages/@react-aria/select/src/HiddenSelect.tsx @@ -30,6 +30,13 @@ export interface AriaHiddenSelectProps { /** HTML form input name. */ name?: string, + /** + * The `<form>` element to associate the input with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string, + /** Sets the disabled state of the select and input. */ isDisabled?: boolean } @@ -65,7 +72,7 @@ export interface HiddenSelectAria { */ export function useHiddenSelect<T>(props: AriaHiddenSelectOptions, state: SelectState<T>, triggerRef: RefObject<FocusableElement | null>): HiddenSelectAria { let data = selectData.get(state) || {}; - let {autoComplete, name = data.name, isDisabled = data.isDisabled} = props; + let {autoComplete, name = data.name, form = data.form, isDisabled = data.isDisabled} = props; let {validationBehavior, isRequired} = data; let {visuallyHiddenProps} = useVisuallyHidden(); @@ -99,6 +106,7 @@ export function useHiddenSelect<T>(props: AriaHiddenSelectOptions, state: Select disabled: isDisabled, required: validationBehavior === 'native' && isRequired, name, + form, value: state.selectedKey ?? undefined, onChange: (e: React.ChangeEvent<HTMLSelectElement>) => state.setSelectedKey(e.target.value) } @@ -110,7 +118,7 @@ export function useHiddenSelect<T>(props: AriaHiddenSelectOptions, state: Select * form autofill, mobile form navigation, and native form submission. */ export function HiddenSelect<T>(props: HiddenSelectProps<T>): JSX.Element | null { - let {state, triggerRef, label, name, isDisabled} = props; + let {state, triggerRef, label, name, form, isDisabled} = props; let selectRef = useRef(null); let {containerProps, selectProps} = useHiddenSelect({...props, selectRef}, state, triggerRef); @@ -146,6 +154,7 @@ export function HiddenSelect<T>(props: HiddenSelectProps<T>): JSX.Element | null type="hidden" autoComplete={selectProps.autoComplete} name={name} + form={form} disabled={isDisabled} value={state.selectedKey ?? ''} /> ); diff --git a/packages/@react-aria/select/src/useSelect.ts b/packages/@react-aria/select/src/useSelect.ts index b9d954bd7d4..35200eabcb3 100644 --- a/packages/@react-aria/select/src/useSelect.ts +++ b/packages/@react-aria/select/src/useSelect.ts @@ -55,6 +55,7 @@ interface SelectData { isDisabled?: boolean, isRequired?: boolean, name?: string, + form?: string, validationBehavior?: 'aria' | 'native' } @@ -72,6 +73,7 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>, isDisabled, isRequired, name, + form, validationBehavior = 'aria' } = props; @@ -142,6 +144,7 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>, isDisabled, isRequired, name, + form, validationBehavior }); diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 75a1d29045c..8f255907077 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -51,7 +51,8 @@ export function useSliderThumb( trackRef, inputRef, orientation = state.orientation, - name + name, + form } = opts; let isDisabled = opts.isDisabled || state.isDisabled; @@ -244,6 +245,7 @@ export function useSliderThumb( step: state.step, value: value, name, + form, disabled: isDisabled, 'aria-orientation': orientation, 'aria-valuetext': state.getThumbValueLabel(index), diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 7a3bd98504f..77abbd59ea1 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -186,6 +186,7 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme maxLength: props.maxLength, minLength: props.minLength, name: props.name, + form: props.form, placeholder: props.placeholder, inputMode: props.inputMode, autoCorrect: props.autoCorrect, diff --git a/packages/@react-aria/toggle/src/useToggle.ts b/packages/@react-aria/toggle/src/useToggle.ts index fee8b760286..32da300efe9 100644 --- a/packages/@react-aria/toggle/src/useToggle.ts +++ b/packages/@react-aria/toggle/src/useToggle.ts @@ -43,6 +43,7 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb isReadOnly = false, value, name, + form, children, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, @@ -94,6 +95,7 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb disabled: isDisabled, ...(value == null ? {} : {value}), name, + form, type: 'checkbox', ...interactions }), diff --git a/packages/@react-spectrum/numberfield/src/NumberField.tsx b/packages/@react-spectrum/numberfield/src/NumberField.tsx index 8cc36864594..44ace71b7c5 100644 --- a/packages/@react-spectrum/numberfield/src/NumberField.tsx +++ b/packages/@react-spectrum/numberfield/src/NumberField.tsx @@ -189,7 +189,7 @@ const NumberFieldInput = React.forwardRef(function NumberFieldInput(props: Numbe <StepButton direction="down" isQuiet={isQuiet} {...decrementProps} /> </> } - {name && <input type="hidden" name={name} value={isNaN(state.numberValue) ? '' : state.numberValue} />} + {name && <input type="hidden" name={name} form={props.form} value={isNaN(state.numberValue) ? '' : state.numberValue} />} </div> </FocusRing> ); diff --git a/packages/@react-spectrum/numberfield/test/NumberField.test.js b/packages/@react-spectrum/numberfield/test/NumberField.test.js index b9ff89c22cf..4ba4883647d 100644 --- a/packages/@react-spectrum/numberfield/test/NumberField.test.js +++ b/packages/@react-spectrum/numberfield/test/NumberField.test.js @@ -2272,10 +2272,11 @@ describe('NumberField', function () { }); it('supports form value', () => { - let {textField, rerender} = renderNumberField({name: 'age', value: 30}); + let {textField, rerender} = renderNumberField({name: 'age', form: 'test', value: 30}); expect(textField).not.toHaveAttribute('name'); let hiddenInput = document.querySelector('input[type=hidden]'); expect(hiddenInput).toHaveAttribute('name', 'age'); + expect(hiddenInput).toHaveAttribute('form', 'test'); expect(hiddenInput).toHaveValue('30'); rerender({name: 'age', value: null}); diff --git a/packages/@react-spectrum/picker/src/Picker.tsx b/packages/@react-spectrum/picker/src/Picker.tsx index 67fca8f89c1..ee3da9e54cc 100644 --- a/packages/@react-spectrum/picker/src/Picker.tsx +++ b/packages/@react-spectrum/picker/src/Picker.tsx @@ -63,6 +63,7 @@ export const Picker = React.forwardRef(function Picker<T extends object>(props: labelPosition = 'top' as LabelPosition, menuWidth, name, + form, autoFocus } = props; @@ -184,7 +185,8 @@ export const Picker = React.forwardRef(function Picker<T extends object>(props: state={state} triggerRef={unwrappedTriggerRef} label={label} - name={name} /> + name={name} + form={form} /> <PressResponder {...mergeProps(hoverProps, triggerProps)}> <FieldButton ref={triggerRef} diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index da7c4ad6960..8f61bd6c013 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -2108,6 +2108,21 @@ describe('Picker', function () { expect(input).toHaveValue('one'); }); + it('should support form prop', () => { + render( + <Provider theme={theme}> + <Picker label="Test" name="picker" form="test"> + <Item key="one">One</Item> + <Item key="two">Two</Item> + <Item key="three">Three</Item> + </Picker> + </Provider> + ); + + let input = document.querySelector('[name=picker]'); + expect(input).toHaveAttribute('form', 'test'); + }); + describe('validation', () => { describe('validationBehavior=native', () => { it('supports isRequired', async () => { diff --git a/packages/@react-spectrum/s2/src/RangeSlider.tsx b/packages/@react-spectrum/s2/src/RangeSlider.tsx index e197db55f1e..c51f0256555 100644 --- a/packages/@react-spectrum/s2/src/RangeSlider.tsx +++ b/packages/@react-spectrum/s2/src/RangeSlider.tsx @@ -34,7 +34,13 @@ export interface RangeSliderProps extends Omit<SliderBaseProps<RangeValue<number /** * The name of the end input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */ - endName?: string + endName?: string, + /** + * The `<form>` element to associate the input with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export const RangeSliderContext = createContext<ContextValue<Partial<RangeSliderProps>, FocusableRefValue<HTMLDivElement>>>(null); @@ -82,6 +88,7 @@ export const RangeSlider = /*#__PURE__*/ forwardRef(function RangeSlider(props: className={thumbContainer} index={0} name={props.startName} + form={props.form} aria-label={stringFormatter.format('slider.minimum')} ref={lowerThumbRef} style={(renderProps) => pressScale(lowerThumbRef, { @@ -103,6 +110,7 @@ export const RangeSlider = /*#__PURE__*/ forwardRef(function RangeSlider(props: className={thumbContainer} index={1} name={props.endName} + form={props.form} aria-label={stringFormatter.format('slider.maximum')} ref={upperThumbRef} style={(renderProps) => pressScale(upperThumbRef, { diff --git a/packages/@react-spectrum/s2/src/Slider.tsx b/packages/@react-spectrum/s2/src/Slider.tsx index 657a17b1bab..0431cee0c5a 100644 --- a/packages/@react-spectrum/s2/src/Slider.tsx +++ b/packages/@react-spectrum/s2/src/Slider.tsx @@ -426,7 +426,7 @@ export const Slider = /*#__PURE__*/ forwardRef(function Slider(props: SliderProp <> <div className={upperTrack({isDisabled, trackStyle})} /> <div style={{width: `${Math.abs(fillWidth) * 100}%`, [cssDirection]: `${offset * 100}%`}} className={filledTrack({isDisabled, isEmphasized, trackStyle})} /> - <SliderThumb className={thumbContainer} index={0} name={props.name} ref={thumbRef} style={(renderProps) => pressScale(thumbRef, {transform: 'translate(-50%, -50%)'})({...renderProps, isPressed: renderProps.isDragging})}> + <SliderThumb className={thumbContainer} index={0} name={props.name} form={props.form} ref={thumbRef} style={(renderProps) => pressScale(thumbRef, {transform: 'translate(-50%, -50%)'})({...renderProps, isPressed: renderProps.isDragging})}> {(renderProps) => ( <div className={thumbHitArea({size})}> <div diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 268d2d647d8..7f6b7405f61 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -62,7 +62,8 @@ export const RangeSlider = React.forwardRef(function RangeSlider(props: Spectrum trackRef={trackRef} inputRef={inputRef} state={state} - name={props.startName} /> + name={props.startName} + form={props.form} /> <div className={classNames(styles, 'spectrum-Slider-track')} style={{ @@ -75,7 +76,8 @@ export const RangeSlider = React.forwardRef(function RangeSlider(props: Spectrum isDisabled={props.isDisabled} trackRef={trackRef} state={state} - name={props.endName} /> + name={props.endName} + form={props.form} /> <div className={classNames(styles, 'spectrum-Slider-track')} style={{ diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 40c36182189..fa531df2a6b 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -107,7 +107,8 @@ export const Slider = React.forwardRef(function Slider(props: SpectrumSliderProp trackRef={trackRef} inputRef={inputRef} state={state} - name={props.name} /> + name={props.name} + form={props.form} /> {filledTrack} {upperTrack} </> diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index 95c7096ba0a..db9d210ee77 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -189,11 +189,13 @@ describe('RangeSlider', function () { }); it('supports form name', () => { - let {getAllByRole} = render(<RangeSlider label="Value" value={{start: 10, end: 40}} startName="minCookies" endName="maxCookies" />); + let {getAllByRole} = render(<RangeSlider label="Value" value={{start: 10, end: 40}} startName="minCookies" endName="maxCookies" form="test" />); let inputs = getAllByRole('slider'); expect(inputs[0]).toHaveAttribute('name', 'minCookies'); + expect(inputs[0]).toHaveAttribute('form', 'test'); expect(inputs[0]).toHaveValue('10'); expect(inputs[1]).toHaveAttribute('name', 'maxCookies'); + expect(inputs[1]).toHaveAttribute('form', 'test'); expect(inputs[1]).toHaveValue('40'); }); diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx index c5bcba26e04..41b255e9d01 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.tsx +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -193,9 +193,10 @@ describe('Slider', function () { }); it('supports form name', () => { - let {getByRole} = render(<Slider label="Value" value={10} name="cookies" />); + let {getByRole} = render(<Slider label="Value" value={10} name="cookies" form="test" />); let input = getByRole('slider'); expect(input).toHaveAttribute('name', 'cookies'); + expect(input).toHaveAttribute('form', 'test'); expect(input).toHaveValue('10'); }); diff --git a/packages/@react-types/button/src/index.d.ts b/packages/@react-types/button/src/index.d.ts index 4c7ee4ffa5a..49a0b65fb08 100644 --- a/packages/@react-types/button/src/index.d.ts +++ b/packages/@react-types/button/src/index.d.ts @@ -68,7 +68,30 @@ interface AriaBaseButtonProps extends FocusableDOMProps, AriaLabelingProps { * Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, * such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. */ - preventFocusOnPress?: boolean + preventFocusOnPress?: boolean, + /** + * The `<form>` element to associate the button with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#form). + */ + form?: string, + /** + * The URL that processes the information submitted by the button. + * Overrides the action attribute of the button's form owner. + */ + formAction?: string, + /** Indicates how to encode the form data that is submitted. */ + formEncType?: string, + /** Indicates the HTTP method used to submit the form. */ + formMethod?: string, + /** Indicates that the form is not to be validated when it is submitted. */ + formNoValidate?: boolean, + /** Overrides the target attribute of the button's form owner. */ + formTarget?: string, + /** Submitted as a pair with the button's value as part of the form data. */ + name?: string, + /** The value associated with the button's name when it's submitted with the form data. */ + value?: string } export interface AriaButtonProps<T extends ElementType = 'button'> extends ButtonProps, LinkButtonProps<T>, AriaBaseButtonProps {} diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index 7350c51497d..51495ca50cc 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -208,7 +208,13 @@ export interface AriaColorAreaProps extends ColorAreaProps, DOMProps, AriaLabeli /** * The name of the y channel input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */ - yName?: string + yName?: string, + /** + * The `<form>` element to associate the ColorArea with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export interface SpectrumColorAreaProps extends AriaColorAreaProps, Omit<StyleProps, 'width' | 'height'> { diff --git a/packages/@react-types/datepicker/src/index.d.ts b/packages/@react-types/datepicker/src/index.d.ts index 15320daab9d..1246d923a9b 100644 --- a/packages/@react-types/datepicker/src/index.d.ts +++ b/packages/@react-types/datepicker/src/index.d.ts @@ -96,7 +96,13 @@ export interface DateRangePickerProps<T extends DateValue> extends Omit<DatePick /** * The name of the end date input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */ - endName?: string + endName?: string, + /** + * The `<form>` element to associate the input with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export interface AriaDateRangePickerProps<T extends DateValue> extends Omit<AriaDatePickerBaseProps<T>, 'validate'>, DateRangePickerProps<T> {} diff --git a/packages/@react-types/select/src/index.d.ts b/packages/@react-types/select/src/index.d.ts index d34a6c9faad..22b6f976801 100644 --- a/packages/@react-types/select/src/index.d.ts +++ b/packages/@react-types/select/src/index.d.ts @@ -47,7 +47,13 @@ export interface AriaSelectProps<T> extends SelectProps<T>, DOMProps, AriaLabeli /** * The name of the input, used when submitting an HTML form. */ - name?: string + name?: string, + /** + * The `<form>` element to associate the input with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export interface SpectrumPickerProps<T> extends AriaSelectProps<T>, AsyncLoadable, SpectrumLabelableProps, StyleProps { diff --git a/packages/@react-types/shared/src/dom.d.ts b/packages/@react-types/shared/src/dom.d.ts index d6acd30ba68..8b8cc36fab3 100644 --- a/packages/@react-types/shared/src/dom.d.ts +++ b/packages/@react-types/shared/src/dom.d.ts @@ -127,7 +127,13 @@ export interface InputDOMProps { /** * The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */ - name?: string + name?: string, + /** + * The `<form>` element to associate the input with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } // DOM props that apply to all text inputs diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index 6603a484636..023516f228e 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -117,5 +117,11 @@ export interface SpectrumRangeSliderProps extends SpectrumBarSliderBase<RangeVal /** * The name of the end input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). */ - endName?: string + endName?: string, + /** + * The `<form>` element to associate the slider with. + * The value of this attribute must be the id of a `<form>` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } diff --git a/packages/react-aria-components/src/Button.tsx b/packages/react-aria-components/src/Button.tsx index 808e169332b..ee843f8cd61 100644 --- a/packages/react-aria-components/src/Button.tsx +++ b/packages/react-aria-components/src/Button.tsx @@ -66,28 +66,6 @@ export interface ButtonRenderProps { } export interface ButtonProps extends Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>, HoverEvents, SlotProps, RenderProps<ButtonRenderProps> { - /** - * The `<form>` element to associate the button with. - * The value of this attribute must be the id of a `<form>` in the same document. - */ - form?: string, - /** - * The URL that processes the information submitted by the button. - * Overrides the action attribute of the button's form owner. - */ - formAction?: string, - /** Indicates how to encode the form data that is submitted. */ - formEncType?: string, - /** Indicates the HTTP method used to submit the form. */ - formMethod?: string, - /** Indicates that the form is not to be validated when it is submitted. */ - formNoValidate?: boolean, - /** Overrides the target attribute of the button's form owner. */ - formTarget?: string, - /** Submitted as a pair with the button's value as part of the form data. */ - name?: string, - /** The value associated with the button's name when it's submitted with the form data. */ - value?: string, /** * Whether the button is in a pending state. This disables press and hover events * while retaining focusability, and announces the pending state to screen readers. @@ -99,8 +77,6 @@ interface ButtonContextValue extends ButtonProps { isPressed?: boolean } -const additionalButtonHTMLAttributes = new Set(['form', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget', 'name', 'value']); - export const ButtonContext = createContext<ContextValue<ButtonContextValue, HTMLButtonElement>>({}); /** @@ -161,7 +137,7 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop // We do this by changing the button's type to button. return ( <button - {...filterDOMProps(props, {propNames: additionalButtonHTMLAttributes})} + {...filterDOMProps(props)} {...mergeProps(buttonProps, focusProps, hoverProps)} {...renderProps} type={buttonProps.type === 'submit' && isPending ? 'button' : buttonProps.type} diff --git a/packages/react-aria-components/src/NumberField.tsx b/packages/react-aria-components/src/NumberField.tsx index 375b9ff7ab0..4450f4cfac4 100644 --- a/packages/react-aria-components/src/NumberField.tsx +++ b/packages/react-aria-components/src/NumberField.tsx @@ -127,7 +127,7 @@ export const NumberField = /*#__PURE__*/ (forwardRef as forwardRefType)(function data-disabled={props.isDisabled || undefined} data-required={props.isRequired || undefined} data-invalid={validation.isInvalid || undefined} /> - {props.name && <input type="hidden" name={props.name} value={isNaN(state.numberValue) ? '' : state.numberValue} />} + {props.name && <input type="hidden" name={props.name} form={props.form} value={isNaN(state.numberValue) ? '' : state.numberValue} />} </Provider> ); }); diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index d96efb4dc23..06cb3e62223 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -220,6 +220,7 @@ function SelectInner<T extends object>({props, selectRef: ref, collection}: Sele triggerRef={buttonRef} label={label} name={props.name} + form={props.form} isDisabled={props.isDisabled} /> </Provider> ); diff --git a/packages/react-aria-components/test/Checkbox.test.js b/packages/react-aria-components/test/Checkbox.test.js index 6754c5dd5ef..5a2e7b969a5 100644 --- a/packages/react-aria-components/test/Checkbox.test.js +++ b/packages/react-aria-components/test/Checkbox.test.js @@ -244,4 +244,10 @@ describe('Checkbox', () => { expect(inputRef.current).toBe(getByRole('checkbox')); expect(contextInputRef.current).toBe(getByRole('checkbox')); }); + + it('should support form prop', () => { + let {getByRole} = render(<Checkbox form="test">Test</Checkbox>); + let checkbox = getByRole('checkbox'); + expect(checkbox).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/CheckboxGroup.test.js b/packages/react-aria-components/test/CheckboxGroup.test.js index 8879d6d506d..dea4796fa24 100644 --- a/packages/react-aria-components/test/CheckboxGroup.test.js +++ b/packages/react-aria-components/test/CheckboxGroup.test.js @@ -281,4 +281,11 @@ describe('CheckboxGroup', () => { expect(onFocusChange).toHaveBeenCalledTimes(2); // triggered by onBlur expect(onFocusChange).toHaveBeenLastCalledWith(false); }); + + it('should support form prop', () => { + let {getAllByRole} = renderGroup({form: 'test'}); + for (let checkbox of getAllByRole('checkbox')) { + expect(checkbox).toHaveAttribute('form', 'test'); + } + }); }); diff --git a/packages/react-aria-components/test/ColorArea.test.js b/packages/react-aria-components/test/ColorArea.test.js index 9633d8dc788..56be2f783d8 100644 --- a/packages/react-aria-components/test/ColorArea.test.js +++ b/packages/react-aria-components/test/ColorArea.test.js @@ -144,4 +144,10 @@ describe('ColorArea', () => { expect(wrapper).toHaveAttribute('data-disabled', 'true'); expect(wrapper).toHaveClass('disabled'); }); + + it('should support form prop', () => { + let {getByRole} = renderColorArea({form: 'test'}); + let input = getByRole('slider'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/ColorField.test.js b/packages/react-aria-components/test/ColorField.test.js index 9e26ef4589a..7b53495c326 100644 --- a/packages/react-aria-components/test/ColorField.test.js +++ b/packages/react-aria-components/test/ColorField.test.js @@ -145,4 +145,13 @@ describe('ColorField', () => { await user.tab(); expect(onChange).toHaveBeenCalledWith(parseColor('hsl(100, 25%, 73.33%)')); }); + + it('should support form prop', () => { + let {getByRole} = render( + <TestColorField form="test" /> + ); + + let input = getByRole('textbox'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/ColorSlider.test.js b/packages/react-aria-components/test/ColorSlider.test.js index cba139981ca..455ff5e87bc 100644 --- a/packages/react-aria-components/test/ColorSlider.test.js +++ b/packages/react-aria-components/test/ColorSlider.test.js @@ -186,4 +186,10 @@ describe('ColorSlider', () => { expect(wrapper).toHaveClass('vertical'); expect(slider).toHaveAttribute('aria-orientation', 'vertical'); }); + + it('should support form prop', () => { + let {getByRole} = renderSlider({form: 'test'}); + let input = getByRole('slider'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/ColorWheel.test.js b/packages/react-aria-components/test/ColorWheel.test.js index 911ddaa4926..971c75e00e9 100644 --- a/packages/react-aria-components/test/ColorWheel.test.js +++ b/packages/react-aria-components/test/ColorWheel.test.js @@ -144,4 +144,10 @@ describe('ColorWheel', () => { expect(wrapper).toHaveAttribute('data-disabled', 'true'); expect(wrapper).toHaveClass('disabled'); }); + + it('should support form prop', () => { + let {getByRole} = renderColorWheel({form: 'test'}); + let input = getByRole('slider'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index 670362f6a65..8b3a2401ff9 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -378,4 +378,10 @@ describe('ComboBox', () => { let text = popover.querySelector('.react-aria-Text'); expect(text).not.toHaveAttribute('id'); }); + + it('should support form prop', () => { + let {getByRole} = render(<TestComboBox form="test" />); + let input = getByRole('combobox'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index bdbd26a1ba7..4efff22ccb6 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -218,7 +218,7 @@ describe('DateField', () => { it('should support form value', () => { render( - <DateField name="birthday" value={new CalendarDate(2020, 2, 3)}> + <DateField name="birthday" form="test" value={new CalendarDate(2020, 2, 3)}> <Label>Birth date</Label> <DateInput> {segment => <DateSegment segment={segment} />} @@ -227,6 +227,7 @@ describe('DateField', () => { ); let input = document.querySelector('input[name=birthday]'); expect(input).toHaveValue('2020-02-03'); + expect(input).toHaveAttribute('form', 'test'); }); it('should render data- attributes only on the outer element', () => { diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 59a0239bc8f..5f60bca8b30 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -153,9 +153,10 @@ describe('DatePicker', () => { }); it('should support form value', () => { - render(<TestDatePicker name="birthday" value={new CalendarDate(2020, 2, 3)} />); + render(<TestDatePicker name="birthday" form="test" value={new CalendarDate(2020, 2, 3)} />); let input = document.querySelector('input[name=birthday]'); expect(input).toHaveValue('2020-02-03'); + expect(input).toHaveAttribute('form', 'test'); }); it('should render data- attributes only on the outer element', () => { diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index aab7089ba2a..f861a9d54fd 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -163,11 +163,13 @@ describe('DateRangePicker', () => { }); it('should support form value', () => { - render(<TestDateRangePicker startName="start" endName="end" value={{start: new CalendarDate(2023, 1, 10), end: new CalendarDate(2023, 1, 20)}} />); + render(<TestDateRangePicker startName="start" endName="end" form="test" value={{start: new CalendarDate(2023, 1, 10), end: new CalendarDate(2023, 1, 20)}} />); let start = document.querySelector('input[name=start]'); expect(start).toHaveValue('2023-01-10'); + expect(start).toHaveAttribute('form', 'test'); let end = document.querySelector('input[name=end]'); expect(end).toHaveValue('2023-01-20'); + expect(end).toHaveAttribute('form', 'test'); }); it('should render data- attributes only on the outer element', () => { diff --git a/packages/react-aria-components/test/NumberField.test.js b/packages/react-aria-components/test/NumberField.test.js index 29de5db0698..14c3e0db770 100644 --- a/packages/react-aria-components/test/NumberField.test.js +++ b/packages/react-aria-components/test/NumberField.test.js @@ -126,11 +126,12 @@ describe('NumberField', () => { }); it('should support form value', () => { - let {rerender} = render(<TestNumberField name="test" value={25} formatOptions={{style: 'currency', currency: 'USD'}} />); + let {rerender} = render(<TestNumberField name="test" form="test" value={25} formatOptions={{style: 'currency', currency: 'USD'}} />); let input = document.querySelector('input[name=test]'); expect(input).toHaveValue('25'); + expect(input).toHaveAttribute('form', 'test'); - rerender(<TestNumberField name="test" value={null} formatOptions={{style: 'currency', currency: 'USD'}} />); + rerender(<TestNumberField name="test" form="test" value={null} formatOptions={{style: 'currency', currency: 'USD'}} />); expect(input).toHaveValue(''); }); diff --git a/packages/react-aria-components/test/RadioGroup.test.js b/packages/react-aria-components/test/RadioGroup.test.js index bab80b346b3..221439757f1 100644 --- a/packages/react-aria-components/test/RadioGroup.test.js +++ b/packages/react-aria-components/test/RadioGroup.test.js @@ -557,4 +557,11 @@ describe('RadioGroup', () => { expect(inputRef.current).toBe(radio); expect(contextInputRef.current).toBe(radio); }); + + it('should support form prop', () => { + let {getAllByRole} = renderGroup({form: 'test'}); + for (let radio of getAllByRole('radio')) { + expect(radio).toHaveAttribute('form', 'test'); + } + }); }); diff --git a/packages/react-aria-components/test/SearchField.test.js b/packages/react-aria-components/test/SearchField.test.js index 6573011c526..736273c8220 100644 --- a/packages/react-aria-components/test/SearchField.test.js +++ b/packages/react-aria-components/test/SearchField.test.js @@ -126,4 +126,13 @@ describe('SearchField', () => { await user.tab(); expect(input).not.toHaveAttribute('aria-describedby'); }); + + it('should support form prop', () => { + let {getByRole} = render( + <TestSearchField form="test" /> + ); + + let input = getByRole('searchbox'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 66b6a213bea..c98378e8afe 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -413,4 +413,13 @@ describe('Select', () => { let text = popover.querySelector('.react-aria-Text'); expect(text).not.toHaveAttribute('id'); }); + + it('should support form prop', () => { + render( + <TestSelect name="select" form="test" /> + ); + + let input = document.querySelector('[name=select]'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/Slider.test.js b/packages/react-aria-components/test/Slider.test.js index 720d2afab98..888cbcc8d9d 100644 --- a/packages/react-aria-components/test/Slider.test.js +++ b/packages/react-aria-components/test/Slider.test.js @@ -274,22 +274,29 @@ describe('Slider', () => { await user.pointer([{target: track, keys: '[MouseLeft]', coords: {x: 20}}]); expect(onChange).toHaveBeenCalled(); }); -}); -it('should support input ref', () => { - let inputRef = React.createRef(); - - let {getByRole} = render( - <Slider> - <Label>Test</Label> - <SliderOutput /> - <SliderTrack> - <SliderThumb inputRef={inputRef} /> - </SliderTrack> - </Slider> - ); - - let group = getByRole('group'); - let thumbInput = group.querySelector('input'); - expect(inputRef.current).toBe(thumbInput); + it('should support input ref', () => { + let inputRef = React.createRef(); + + let {getByRole} = render( + <Slider> + <Label>Test</Label> + <SliderOutput /> + <SliderTrack> + <SliderThumb inputRef={inputRef} /> + </SliderTrack> + </Slider> + ); + + let group = getByRole('group'); + let thumbInput = group.querySelector('input'); + expect(inputRef.current).toBe(thumbInput); + }); + + it('should support form prop', () => { + let {getByRole} = renderSlider({}, {form: 'test'}); + let input = getByRole('slider'); + expect(input).toHaveAttribute('form', 'test'); + }); }); + diff --git a/packages/react-aria-components/test/Switch.test.js b/packages/react-aria-components/test/Switch.test.js index 987088625a8..748471e9a0e 100644 --- a/packages/react-aria-components/test/Switch.test.js +++ b/packages/react-aria-components/test/Switch.test.js @@ -229,4 +229,10 @@ describe('Switch', () => { expect(inputRef.current).toBe(getByRole('switch')); expect(contextInputRef.current).toBe(getByRole('switch')); }); + + it('should support form prop', () => { + let {getByRole} = render(<Switch form="test">Test</Switch>); + let input = getByRole('switch'); + expect(input).toHaveAttribute('form', 'test'); + }); }); diff --git a/packages/react-aria-components/test/TextField.test.js b/packages/react-aria-components/test/TextField.test.js index 527c6644b5a..a5d354ee4c7 100644 --- a/packages/react-aria-components/test/TextField.test.js +++ b/packages/react-aria-components/test/TextField.test.js @@ -257,5 +257,14 @@ describe('TextField', () => { expect(input).toHaveAttribute('id', 'name'); expect(label).toHaveAttribute('for', 'name'); }); + + it('should support form prop', () => { + let {getByRole} = render( + <TestTextField form="test" input={component} /> + ); + + let input = getByRole('textbox'); + expect(input).toHaveAttribute('form', 'test'); + }); }); }); diff --git a/packages/react-aria-components/test/TimeField.test.js b/packages/react-aria-components/test/TimeField.test.js index 9315b8b91cb..80e85e481a8 100644 --- a/packages/react-aria-components/test/TimeField.test.js +++ b/packages/react-aria-components/test/TimeField.test.js @@ -133,7 +133,7 @@ describe('TimeField', () => { it('should support form value', () => { render( - <TimeField name="time" value={new Time(8, 30)}> + <TimeField name="time" form="test" value={new Time(8, 30)}> <Label>Time</Label> <DateInput> {segment => <DateSegment segment={segment} />} @@ -142,6 +142,7 @@ describe('TimeField', () => { ); let input = document.querySelector('input[name=time]'); expect(input).toHaveValue('08:30:00'); + expect(input).toHaveAttribute('form', 'test'); }); it('supports validation errors', async () => { From cb63c90758892ef19f9c2bfbe411df63b482d573 Mon Sep 17 00:00:00 2001 From: Devon Govett <devongovett@gmail.com> Date: Wed, 25 Jun 2025 13:48:13 -0700 Subject: [PATCH 2/2] add missing --- packages/@react-spectrum/color/src/ColorField.tsx | 2 +- packages/@react-spectrum/combobox/src/ComboBox.tsx | 2 +- packages/react-aria-components/src/ColorField.tsx | 2 +- packages/react-aria-components/src/ComboBox.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/color/src/ColorField.tsx b/packages/@react-spectrum/color/src/ColorField.tsx index 4e4c0f79100..db92d864122 100644 --- a/packages/@react-spectrum/color/src/ColorField.tsx +++ b/packages/@react-spectrum/color/src/ColorField.tsx @@ -73,7 +73,7 @@ function ColorChannelField(props: ColorChannelFieldProps) { inputRef={inputRef} {...result} inputClassName={classNames(styles, 'react-spectrum-ColorField-input')} /> - {props.name && <input type="hidden" name={props.name} value={isNaN(state.numberValue) ? '' : state.numberValue} />} + {props.name && <input type="hidden" name={props.name} form={props.form} value={isNaN(state.numberValue) ? '' : state.numberValue} />} </> ); } diff --git a/packages/@react-spectrum/combobox/src/ComboBox.tsx b/packages/@react-spectrum/combobox/src/ComboBox.tsx index 41cc76bffaf..16f5255ce83 100644 --- a/packages/@react-spectrum/combobox/src/ComboBox.tsx +++ b/packages/@react-spectrum/combobox/src/ComboBox.tsx @@ -175,7 +175,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase(props: SpectrumCombo validationState={props.validationState || (isInvalid ? 'invalid' : undefined)} ref={inputGroupRef} /> </Field> - {name && formValue === 'key' && <input type="hidden" name={name} value={state.selectedKey ?? ''} />} + {name && formValue === 'key' && <input type="hidden" name={name} form={props.form} value={state.selectedKey ?? ''} />} <Popover state={state} UNSAFE_style={style} diff --git a/packages/react-aria-components/src/ColorField.tsx b/packages/react-aria-components/src/ColorField.tsx index 3ba163b0031..47a08b64342 100644 --- a/packages/react-aria-components/src/ColorField.tsx +++ b/packages/react-aria-components/src/ColorField.tsx @@ -114,7 +114,7 @@ function ColorChannelField(props: ColorChannelFieldProps) { errorMessageProps, validation )} - {props.name && <input type="hidden" name={props.name} value={isNaN(state.numberValue) ? '' : state.numberValue} />} + {props.name && <input type="hidden" name={props.name} form={props.form} value={isNaN(state.numberValue) ? '' : state.numberValue} />} </> ); } diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index a3804b1d5a9..4c348f789a8 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -224,7 +224,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}: data-disabled={props.isDisabled || undefined} data-invalid={validation.isInvalid || undefined} data-required={props.isRequired || undefined} /> - {name && formValue === 'key' && <input type="hidden" name={name} value={state.selectedKey ?? ''} />} + {name && formValue === 'key' && <input type="hidden" name={name} form={props.form} value={state.selectedKey ?? ''} />} </Provider> ); }