From a19f04d14bf89062adb4dea6a64994d41ab4752d Mon Sep 17 00:00:00 2001 From: Devon Govett 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, 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) => { 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(props: AriaDateFieldOptions let inputProps: InputHTMLAttributes = { 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(props: AriaDatePickerProps // 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(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(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 `
` element to associate the input with. + * The value of this attribute must be the id of a `` 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(props: AriaHiddenSelectOptions, state: SelectState, triggerRef: RefObject): 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(props: AriaHiddenSelectOptions, state: Select disabled: isDisabled, required: validationBehavior === 'native' && isRequired, name, + form, value: state.selectedKey ?? undefined, onChange: (e: React.ChangeEvent) => state.setSelectedKey(e.target.value) } @@ -110,7 +118,7 @@ export function useHiddenSelect(props: AriaHiddenSelectOptions, state: Select * form autofill, mobile form navigation, and native form submission. */ export function HiddenSelect(props: HiddenSelectProps): 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(props: HiddenSelectProps): 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(props: AriaSelectOptions, state: SelectState, isDisabled, isRequired, name, + form, validationBehavior = 'aria' } = props; @@ -142,6 +144,7 @@ export function useSelect(props: AriaSelectOptions, state: SelectState, 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 } - {name && } + {name && } ); 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(props: labelPosition = 'top' as LabelPosition, menuWidth, name, + form, autoFocus } = props; @@ -184,7 +185,8 @@ export const Picker = React.forwardRef(function Picker(props: state={state} triggerRef={unwrappedTriggerRef} label={label} - name={name} /> + name={name} + form={form} /> { + render( + + + One + Two + Three + + + ); + + 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` element to associate the input with. + * The value of this attribute must be the id of a `` 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, FocusableRefValue>>(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 <>
- pressScale(thumbRef, {transform: 'translate(-50%, -50%)'})({...renderProps, isPressed: renderProps.isDragging})}> + pressScale(thumbRef, {transform: 'translate(-50%, -50%)'})({...renderProps, isPressed: renderProps.isDragging})}> {(renderProps) => (
+ name={props.startName} + form={props.form} />
+ name={props.endName} + form={props.form} />
+ 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(); + let {getAllByRole} = render(); 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(); + let {getByRole} = render(); 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 `` element to associate the button with. + * The value of this attribute must be the id of a `` 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 extends ButtonProps, LinkButtonProps, 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 `` element to associate the ColorArea with. + * The value of this attribute must be the id of a `` 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 { 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 extends Omit` element to associate the input with. + * The value of this attribute must be the id of a `` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export interface AriaDateRangePickerProps extends Omit, 'validate'>, DateRangePickerProps {} 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 extends SelectProps, DOMProps, AriaLabeli /** * The name of the input, used when submitting an HTML form. */ - name?: string + name?: string, + /** + * The `` element to associate the input with. + * The value of this attribute must be the id of a `` in the same document. + * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form). + */ + form?: string } export interface SpectrumPickerProps extends AriaSelectProps, 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 `` element to associate the input with. + * The value of this attribute must be the id of a `` 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` element to associate the slider with. + * The value of this attribute must be the id of a `` 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, HoverEvents, SlotProps, RenderProps { - /** - * The `` element to associate the button with. - * The value of this attribute must be the id of a `` 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>({}); /** @@ -161,7 +137,7 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop // We do this by changing the button's type to button. return (