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 0c282a16a1f..cc91277bfe3 100644 --- a/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts +++ b/packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts @@ -44,7 +44,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. @@ -73,6 +73,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 fb995468299..9d06b86e2fb 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 cb907543e34..13d56119eef 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'); @@ -426,6 +427,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, @@ -451,6 +453,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 7c627e8f92c..55373448c9b 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 12095745831..5a04a465421 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 25e650415a3..0196392c2ff 100644 --- a/packages/@react-aria/datepicker/src/useDatePicker.ts +++ b/packages/@react-aria/datepicker/src/useDatePicker.ts @@ -150,7 +150,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 93b3a698cc8..66424530f52 100644 --- a/packages/@react-aria/datepicker/src/useDateRangePicker.ts +++ b/packages/@react-aria/datepicker/src/useDateRangePicker.ts @@ -187,6 +187,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, @@ -205,6 +206,7 @@ export function useDateRangePicker(props: AriaDateRangePick defaultValue: state.defaultValue?.end, 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 ca4548d6abe..08da12b97a5 100644 --- a/packages/@react-aria/numberfield/src/useNumberField.ts +++ b/packages/@react-aria/numberfield/src/useNumberField.ts @@ -199,7 +199,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 babcb48e2d6..ffcb68a82b7 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.defaultSelectedValue, 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 3e0effa0ddc..8529685e9e2 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 f6fae6e5445..7011cc74cf7 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(); @@ -102,6 +109,7 @@ export function useHiddenSelect(props: AriaHiddenSelectOptions, state: Select disabled: isDisabled, required: validationBehavior === 'native' && isRequired, name, + form, value: state.selectedKey ?? '', onChange, onInput: onChange @@ -114,7 +122,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); @@ -150,6 +158,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 a196b38a5ee..5d283972ecf 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 c0a40b6d178..5a609e4f78b 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 cadb0e2d43b..e232514d052 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -188,6 +188,7 @@ export function useTextField - {props.name && } + {props.name && } ); } 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} /> - {name && formValue === 'key' && } + {name && formValue === 'key' && } } - {name && } + {name && } ); diff --git a/packages/@react-spectrum/numberfield/test/NumberField.test.js b/packages/@react-spectrum/numberfield/test/NumberField.test.js index d6ff65bcd57..73f1c306fff 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'); + }); + if (parseInt(React.version, 10) >= 19) { it('resets to defaultSelectedKey when submitting form action', async () => { function Test() { diff --git a/packages/@react-spectrum/s2/src/RangeSlider.tsx b/packages/@react-spectrum/s2/src/RangeSlider.tsx index ea0db19002e..f37322285af 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); @@ -89,6 +95,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, { @@ -110,6 +117,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 08d3fe8f140..b3a9da22b2b 100644 --- a/packages/@react-spectrum/s2/src/Slider.tsx +++ b/packages/@react-spectrum/s2/src/Slider.tsx @@ -429,7 +429,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 ca29dadce04..29f9906a7ad 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 c78e73ca934..0cf7f8831e3 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 (
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(Test); + 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( + + ); + + 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(); + 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 a1907f2605f..ac5013cc57b 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( - + {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(); + render(); 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 cdab18176fe..e6562776338 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -179,11 +179,13 @@ describe('DateRangePicker', () => { }); it('should support form value', () => { - render(); + render(); 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(); + let {rerender} = render(); let input = document.querySelector('input[name=test]'); expect(input).toHaveValue('25'); + expect(input).toHaveAttribute('form', 'test'); - rerender(); + rerender(); 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( + + ); + + 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 befa0735e98..1812d003583 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -414,6 +414,15 @@ describe('Select', () => { expect(text).not.toHaveAttribute('id'); }); + it('should support form prop', () => { + render( + + ); + + let input = document.querySelector('[name=select]'); + expect(input).toHaveAttribute('form', 'test'); + }); + it('should not submit if required and selectedKey is null', async () => { const onSubmit = jest.fn().mockImplementation(e => e.preventDefault()); 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( - - - - - - - - ); - - 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( + + + + + + + + ); + + 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(Test); + 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( + + ); + + 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( - + {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 () => {