diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 462d61357b..893c5e8b04 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
+### Added
+
+- Add `form` prop to form-like components such as `RadioGroup`, `Switch`, `Listbox`, and `Combobox` ([#2356](https://github.com/tailwindlabs/headlessui/pull/2356))
+
## [1.7.13] - 2023-03-03
### Fixed
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
index fcc94ff4e7..76ea73616b 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
@@ -5700,6 +5700,51 @@ describe('Multi-select', () => {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ function Example() {
+ let [value, setValue] = useState(null)
+ return (
+
+
+
+ Trigger
+ Pizza Delivery
+
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+
+
+ )
+ }
+
+ render()
+
+ // Open combobox
+ await click(getComboboxButton())
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index 0302d2cc3f..d7fd6319fd 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -380,6 +380,7 @@ export type ComboboxProps<
> = ComboboxValueProps & {
disabled?: boolean
__demoMode?: boolean
+ form?: string
name?: string
}
@@ -408,6 +409,7 @@ function ComboboxFn a === z,
disabled = false,
@@ -671,6 +673,7 @@ function ComboboxFn {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ function Example() {
+ let [value, setValue] = useState(null)
+ return (
+
+
+ Trigger
+ Pizza Delivery
+
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+
+
+ )
+ }
+
+ render()
+
+ // Open listbox
+ await click(getListboxButton())
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index db0b36a5be..ad60c66b5e 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -343,6 +343,7 @@ export type ListboxProps = Props<
by?: (keyof TActualType & string) | ((a: TActualType, z: TActualType) => boolean)
disabled?: boolean
horizontal?: boolean
+ form?: string
name?: string
multiple?: boolean
}
@@ -355,6 +356,7 @@ function ListboxFn<
let {
value: controlledValue,
defaultValue,
+ form: formName,
name,
onChange: controlledOnChange,
by = (a: TActualType, z: TActualType) => a === z,
@@ -565,6 +567,7 @@ function ListboxFn<
type: 'hidden',
hidden: true,
readOnly: true,
+ form: formName,
name,
value,
})}
diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx
index 94bb93566d..d823ce8f1b 100644
--- a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx
+++ b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx
@@ -1356,6 +1356,47 @@ describe('Mouse interactions', () => {
})
describe('Form compatibility', () => {
+ it(
+ 'should be possible to set the `form`, which is forwarded to the hidden inputs',
+ suppressConsoleLogs(async () => {
+ let submits = jest.fn()
+
+ function Example() {
+ let [value, setValue] = useState(null)
+ return (
+
+
+ Pizza Delivery
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+
+ )
+ }
+
+ render()
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+ )
+
it(
'should be possible to submit a form with a value',
suppressConsoleLogs(async () => {
diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
index d9c34f9d24..2356145abf 100644
--- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
+++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
@@ -147,6 +147,7 @@ export type RadioGroupProps = Props<
onChange?(value: TType): void
by?: (keyof TType & string) | ((a: TType, z: TType) => boolean)
disabled?: boolean
+ form?: string
name?: string
}
>
@@ -160,6 +161,7 @@ function RadioGroupFn a === z,
@@ -343,6 +345,7 @@ function RadioGroupFn {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ function Example() {
+ let [state, setState] = useState(false)
+ return (
+
+
+
+ Enable notifications
+
+
+
+
+ )
+ }
+
+ render()
+
+ // Toggle
+ await click(getSwitchLabel())
+
+ // Submit the form again
+ await click(getByText('Submit'))
+
+ // Verify that the form has been submitted
+ expect(submits).lastCalledWith([['notifications', 'on']])
+ })
+
it('should be possible to submit a form with an boolean value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx
index 3d6f3975fd..ede476f15a 100644
--- a/packages/@headlessui-react/src/components/switch/switch.tsx
+++ b/packages/@headlessui-react/src/components/switch/switch.tsx
@@ -112,6 +112,7 @@ export type SwitchProps = Props<
onChange?(checked: boolean): void
name?: string
value?: string
+ form?: string
}
>
@@ -127,6 +128,7 @@ function SwitchFn(
onChange: controlledOnChange,
name,
value,
+ form,
...theirProps
} = props
let groupContext = useContext(GroupContext)
@@ -193,6 +195,7 @@ function SwitchFn(
type: 'checkbox',
hidden: true,
readOnly: true,
+ form,
checked,
name,
value,
diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md
index 8f938fd986..4737021c20 100644
--- a/packages/@headlessui-vue/CHANGELOG.md
+++ b/packages/@headlessui-vue/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
+### Added
+
+- Add `form` prop to form-like components such as `RadioGroup`, `Switch`, `Listbox`, and `Combobox` ([#2356](https://github.com/tailwindlabs/headlessui/pull/2356))
+
## [1.7.12] - 2023-03-03
### Fixed
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index 0fd8168d95..a909b88249 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -5926,6 +5926,50 @@ describe('Multi-select', () => {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ renderTemplate({
+ template: html`
+
+
+
+ Trigger
+
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+
+ `,
+ setup: () => {
+ let value = ref(null)
+ return {
+ value,
+ handleSubmit(event: SubmitEvent) {
+ event.preventDefault()
+ submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
+ },
+ }
+ },
+ })
+
+ // Open combobox
+ await click(getComboboxButton())
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts
index 2fb12fe427..0adc6ce0f2 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts
@@ -132,7 +132,8 @@ export let Combobox = defineComponent({
>,
default: undefined,
},
- name: { type: String },
+ form: { type: String, optional: true },
+ name: { type: String, optional: true },
nullable: { type: Boolean, default: false },
multiple: { type: [Boolean], default: false },
},
@@ -466,7 +467,7 @@ export let Combobox = defineComponent({
})
return () => {
- let { name, disabled, ...theirProps } = props
+ let { name, disabled, form, ...theirProps } = props
let slot = {
open: comboboxState.value === ComboboxStates.Open,
disabled,
@@ -487,6 +488,7 @@ export let Combobox = defineComponent({
type: 'hidden',
hidden: true,
readOnly: true,
+ form,
name,
value,
})
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
index def8f86912..a40bbd1341 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
@@ -4897,6 +4897,49 @@ describe('Multi-select', () => {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ renderTemplate({
+ template: html`
+
+
+ Trigger
+
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+
+ `,
+ setup: () => {
+ let value = ref(null)
+ return {
+ value,
+ handleSubmit(event: SubmitEvent) {
+ event.preventDefault()
+ submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
+ },
+ }
+ },
+ })
+
+ // Open listbox
+ await click(getListboxButton())
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts
index feca0fe85f..30c86f6b33 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.ts
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts
@@ -133,6 +133,7 @@ export let Listbox = defineComponent({
>,
default: undefined,
},
+ form: { type: String, optional: true },
name: { type: String, optional: true },
multiple: { type: [Boolean], default: false },
},
@@ -369,7 +370,7 @@ export let Listbox = defineComponent({
})
return () => {
- let { name, modelValue, disabled, ...theirProps } = props
+ let { name, modelValue, disabled, form, ...theirProps } = props
let slot = { open: listboxState.value === ListboxStates.Open, disabled, value: value.value }
@@ -385,6 +386,7 @@ export let Listbox = defineComponent({
type: 'hidden',
hidden: true,
readOnly: true,
+ form,
name,
value,
})
diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts
index 5bee9efd73..6cb0e4ce59 100644
--- a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts
+++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts
@@ -1531,6 +1531,48 @@ describe('Mouse interactions', () => {
})
describe('Form compatibility', () => {
+ it(
+ 'should be possible to set the `form`, which is forwarded to the hidden inputs',
+ suppressConsoleLogs(async () => {
+ let submits = jest.fn()
+
+ renderTemplate({
+ template: html`
+
+
+ Pizza Delivery
+ Pickup
+ Home delivery
+ Dine in
+
+
+
+ `,
+ setup() {
+ let deliveryMethod = ref(null)
+ return {
+ deliveryMethod,
+ handleSubmit(event: SubmitEvent) {
+ event.preventDefault()
+
+ submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
+ },
+ }
+ },
+ })
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Submit the form
+ await click(getByText('Submit'))
+
+ expect(submits).lastCalledWith([['delivery', 'pickup']])
+ })
+ )
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts
index fca774149b..a198d19975 100644
--- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts
+++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts
@@ -80,6 +80,7 @@ export let RadioGroup = defineComponent({
by: { type: [String, Function], default: () => defaultComparator },
modelValue: { type: [Object, String, Number, Boolean], default: undefined },
defaultValue: { type: [Object, String, Number, Boolean], default: undefined },
+ form: { type: String, optional: true },
name: { type: String, optional: true },
id: { type: String, default: () => `headlessui-radiogroup-${useId()}` },
},
@@ -239,7 +240,7 @@ export let RadioGroup = defineComponent({
})
return () => {
- let { disabled, name, id, ...theirProps } = props
+ let { disabled, name, id, form, ...theirProps } = props
let ourProps = {
ref: radioGroupRef,
@@ -262,6 +263,7 @@ export let RadioGroup = defineComponent({
type: 'hidden',
hidden: true,
readOnly: true,
+ form,
name,
value,
})
diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx
index bdac5fb0e9..a67d708340 100644
--- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx
+++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx
@@ -748,6 +748,43 @@ describe('Mouse interactions', () => {
})
describe('Form compatibility', () => {
+ it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
+ let submits = jest.fn()
+
+ renderTemplate({
+ template: html`
+
+
+
+ Enable notifications
+
+
+
+ `,
+ setup() {
+ let checked = ref(false)
+ return {
+ checked,
+ handleSubmit(event: SubmitEvent) {
+ event.preventDefault()
+ submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
+ },
+ }
+ },
+ })
+
+ // Toggle
+ await click(getSwitchLabel())
+
+ // Submit the form again
+ await click(getByText('Submit'))
+
+ // Verify that the form has been submitted
+ expect(submits).lastCalledWith([['notifications', 'on']])
+ })
+
it('should be possible to submit a form with an boolean value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts
index b2416d1897..bb4d11dd14 100644
--- a/packages/@headlessui-vue/src/components/switch/switch.ts
+++ b/packages/@headlessui-vue/src/components/switch/switch.ts
@@ -77,6 +77,7 @@ export let Switch = defineComponent({
as: { type: [Object, String], default: 'button' },
modelValue: { type: Boolean, default: undefined },
defaultChecked: { type: Boolean, optional: true },
+ form: { type: String, optional: true },
name: { type: String, optional: true },
value: { type: String, optional: true },
id: { type: String, default: () => `headlessui-switch-${useId()}` },
@@ -145,7 +146,7 @@ export let Switch = defineComponent({
})
return () => {
- let { id, name, value, ...theirProps } = props
+ let { id, name, value, form, ...theirProps } = props
let slot = { checked: checked.value }
let ourProps = {
id,
@@ -172,6 +173,7 @@ export let Switch = defineComponent({
hidden: true,
readOnly: true,
checked: checked.value,
+ form,
name,
value,
})