Skip to content

Commit fb612f7

Browse files
Add form prop to form-like components such as RadioGroup, Switch, Listbox, and Combobox (#2356)
* Adds form prop to Switch component * add `form` prop to `Switch` component in Vue + tests for both React and Vue * add `form` prop to `Combobox` component * add `form` prop to `Listbox` comopnent * add `form` prop to `RadioGroup` component * update changelog * add Oxford comma * cleanup `screen` import --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent 0c0601f commit fb612f7

File tree

18 files changed

+366
-5
lines changed

18 files changed

+366
-5
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
1313

14+
### Added
15+
16+
- Add `form` prop to form-like components such as `RadioGroup`, `Switch`, `Listbox`, and `Combobox` ([#2356](https://github.com/tailwindlabs/headlessui/pull/2356))
17+
1418
## [1.7.13] - 2023-03-03
1519

1620
### Fixed

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5700,6 +5700,51 @@ describe('Multi-select', () => {
57005700
})
57015701

57025702
describe('Form compatibility', () => {
5703+
it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
5704+
let submits = jest.fn()
5705+
5706+
function Example() {
5707+
let [value, setValue] = useState(null)
5708+
return (
5709+
<div>
5710+
<Combobox form="my-form" value={value} onChange={setValue} name="delivery">
5711+
<Combobox.Input onChange={NOOP} />
5712+
<Combobox.Button>Trigger</Combobox.Button>
5713+
<Combobox.Label>Pizza Delivery</Combobox.Label>
5714+
<Combobox.Options>
5715+
<Combobox.Option value="pickup">Pickup</Combobox.Option>
5716+
<Combobox.Option value="home-delivery">Home delivery</Combobox.Option>
5717+
<Combobox.Option value="dine-in">Dine in</Combobox.Option>
5718+
</Combobox.Options>
5719+
</Combobox>
5720+
5721+
<form
5722+
id="my-form"
5723+
onSubmit={(event) => {
5724+
event.preventDefault()
5725+
submits([...new FormData(event.currentTarget).entries()])
5726+
}}
5727+
>
5728+
<button>Submit</button>
5729+
</form>
5730+
</div>
5731+
)
5732+
}
5733+
5734+
render(<Example />)
5735+
5736+
// Open combobox
5737+
await click(getComboboxButton())
5738+
5739+
// Choose pickup
5740+
await click(getByText('Pickup'))
5741+
5742+
// Submit the form
5743+
await click(getByText('Submit'))
5744+
5745+
expect(submits).lastCalledWith([['delivery', 'pickup']])
5746+
})
5747+
57035748
it('should be possible to submit a form with a value', async () => {
57045749
let submits = jest.fn()
57055750

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ export type ComboboxProps<
380380
> = ComboboxValueProps<TValue, TNullable, TMultiple, TTag> & {
381381
disabled?: boolean
382382
__demoMode?: boolean
383+
form?: string
383384
name?: string
384385
}
385386

@@ -408,6 +409,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
408409
value: controlledValue,
409410
defaultValue,
410411
onChange: controlledOnChange,
412+
form: formName,
411413
name,
412414
by = (a: TValue, z: TValue) => a === z,
413415
disabled = false,
@@ -671,6 +673,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
671673
type: 'hidden',
672674
hidden: true,
673675
readOnly: true,
676+
form: formName,
674677
name,
675678
value,
676679
})}

packages/@headlessui-react/src/components/listbox/listbox.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4716,6 +4716,50 @@ describe('Multi-select', () => {
47164716
})
47174717

47184718
describe('Form compatibility', () => {
4719+
it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
4720+
let submits = jest.fn()
4721+
4722+
function Example() {
4723+
let [value, setValue] = useState(null)
4724+
return (
4725+
<div>
4726+
<Listbox form="my-form" value={value} onChange={setValue} name="delivery">
4727+
<Listbox.Button>Trigger</Listbox.Button>
4728+
<Listbox.Label>Pizza Delivery</Listbox.Label>
4729+
<Listbox.Options>
4730+
<Listbox.Option value="pickup">Pickup</Listbox.Option>
4731+
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
4732+
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
4733+
</Listbox.Options>
4734+
</Listbox>
4735+
4736+
<form
4737+
id="my-form"
4738+
onSubmit={(event) => {
4739+
event.preventDefault()
4740+
submits([...new FormData(event.currentTarget).entries()])
4741+
}}
4742+
>
4743+
<button>Submit</button>
4744+
</form>
4745+
</div>
4746+
)
4747+
}
4748+
4749+
render(<Example />)
4750+
4751+
// Open listbox
4752+
await click(getListboxButton())
4753+
4754+
// Choose pickup
4755+
await click(getByText('Pickup'))
4756+
4757+
// Submit the form
4758+
await click(getByText('Submit'))
4759+
4760+
expect(submits).lastCalledWith([['delivery', 'pickup']])
4761+
})
4762+
47194763
it('should be possible to submit a form with a value', async () => {
47204764
let submits = jest.fn()
47214765

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export type ListboxProps<TTag extends ElementType, TType, TActualType> = Props<
343343
by?: (keyof TActualType & string) | ((a: TActualType, z: TActualType) => boolean)
344344
disabled?: boolean
345345
horizontal?: boolean
346+
form?: string
346347
name?: string
347348
multiple?: boolean
348349
}
@@ -355,6 +356,7 @@ function ListboxFn<
355356
let {
356357
value: controlledValue,
357358
defaultValue,
359+
form: formName,
358360
name,
359361
onChange: controlledOnChange,
360362
by = (a: TActualType, z: TActualType) => a === z,
@@ -565,6 +567,7 @@ function ListboxFn<
565567
type: 'hidden',
566568
hidden: true,
567569
readOnly: true,
570+
form: formName,
568571
name,
569572
value,
570573
})}

packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,47 @@ describe('Mouse interactions', () => {
13561356
})
13571357

13581358
describe('Form compatibility', () => {
1359+
it(
1360+
'should be possible to set the `form`, which is forwarded to the hidden inputs',
1361+
suppressConsoleLogs(async () => {
1362+
let submits = jest.fn()
1363+
1364+
function Example() {
1365+
let [value, setValue] = useState(null)
1366+
return (
1367+
<div>
1368+
<RadioGroup form="my-form" value={value} onChange={setValue} name="delivery">
1369+
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
1370+
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
1371+
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
1372+
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
1373+
</RadioGroup>
1374+
1375+
<form
1376+
id="my-form"
1377+
onSubmit={(event) => {
1378+
event.preventDefault()
1379+
submits([...new FormData(event.currentTarget).entries()])
1380+
}}
1381+
>
1382+
<button>Submit</button>
1383+
</form>
1384+
</div>
1385+
)
1386+
}
1387+
1388+
render(<Example />)
1389+
1390+
// Choose pickup
1391+
await click(getByText('Pickup'))
1392+
1393+
// Submit the form
1394+
await click(getByText('Submit'))
1395+
1396+
expect(submits).lastCalledWith([['delivery', 'pickup']])
1397+
})
1398+
)
1399+
13591400
it(
13601401
'should be possible to submit a form with a value',
13611402
suppressConsoleLogs(async () => {

packages/@headlessui-react/src/components/radio-group/radio-group.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export type RadioGroupProps<TTag extends ElementType, TType> = Props<
147147
onChange?(value: TType): void
148148
by?: (keyof TType & string) | ((a: TType, z: TType) => boolean)
149149
disabled?: boolean
150+
form?: string
150151
name?: string
151152
}
152153
>
@@ -160,6 +161,7 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
160161
id = `headlessui-radiogroup-${internalId}`,
161162
value: controlledValue,
162163
defaultValue,
164+
form: formName,
163165
name,
164166
onChange: controlledOnChange,
165167
by = (a: TType, z: TType) => a === z,
@@ -343,6 +345,7 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
343345
checked: value != null,
344346
hidden: true,
345347
readOnly: true,
348+
form: formName,
346349
name,
347350
value,
348351
})}

packages/@headlessui-react/src/components/switch/switch.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,43 @@ describe('Mouse interactions', () => {
624624
})
625625

626626
describe('Form compatibility', () => {
627+
it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
628+
let submits = jest.fn()
629+
630+
function Example() {
631+
let [state, setState] = useState(false)
632+
return (
633+
<div>
634+
<Switch.Group>
635+
<Switch form="my-form" checked={state} onChange={setState} name="notifications" />
636+
<Switch.Label>Enable notifications</Switch.Label>
637+
</Switch.Group>
638+
639+
<form
640+
id="my-form"
641+
onSubmit={(event) => {
642+
event.preventDefault()
643+
submits([...new FormData(event.currentTarget).entries()])
644+
}}
645+
>
646+
<button>Submit</button>
647+
</form>
648+
</div>
649+
)
650+
}
651+
652+
render(<Example />)
653+
654+
// Toggle
655+
await click(getSwitchLabel())
656+
657+
// Submit the form again
658+
await click(getByText('Submit'))
659+
660+
// Verify that the form has been submitted
661+
expect(submits).lastCalledWith([['notifications', 'on']])
662+
})
663+
627664
it('should be possible to submit a form with an boolean value', async () => {
628665
let submits = jest.fn()
629666

packages/@headlessui-react/src/components/switch/switch.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export type SwitchProps<TTag extends ElementType> = Props<
112112
onChange?(checked: boolean): void
113113
name?: string
114114
value?: string
115+
form?: string
115116
}
116117
>
117118

@@ -127,6 +128,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
127128
onChange: controlledOnChange,
128129
name,
129130
value,
131+
form,
130132
...theirProps
131133
} = props
132134
let groupContext = useContext(GroupContext)
@@ -193,6 +195,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
193195
type: 'checkbox',
194196
hidden: true,
195197
readOnly: true,
198+
form,
196199
checked,
197200
name,
198201
value,

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
1313

14+
### Added
15+
16+
- Add `form` prop to form-like components such as `RadioGroup`, `Switch`, `Listbox`, and `Combobox` ([#2356](https://github.com/tailwindlabs/headlessui/pull/2356))
17+
1418
## [1.7.12] - 2023-03-03
1519

1620
### Fixed

0 commit comments

Comments
 (0)