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 + + + +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + +
+
+ ) + } + + 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 + + + +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + +
+
+ ) + } + + 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 + + +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + +
+
+ ) + } + + 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 + + +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + +
+
+ ) + } + + 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, })