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 <malfait.robin@gmail.com>
This commit is contained in:
Arber Sylejmani
2023-03-14 13:36:49 +01:00
committed by GitHub
parent 0c0601f87a
commit fb612f7580
18 changed files with 366 additions and 5 deletions
+4
View File
@@ -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
@@ -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 (
<div>
<Combobox form="my-form" value={value} onChange={setValue} name="delivery">
<Combobox.Input onChange={NOOP} />
<Combobox.Button>Trigger</Combobox.Button>
<Combobox.Label>Pizza Delivery</Combobox.Label>
<Combobox.Options>
<Combobox.Option value="pickup">Pickup</Combobox.Option>
<Combobox.Option value="home-delivery">Home delivery</Combobox.Option>
<Combobox.Option value="dine-in">Dine in</Combobox.Option>
</Combobox.Options>
</Combobox>
<form
id="my-form"
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<button>Submit</button>
</form>
</div>
)
}
render(<Example />)
// 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()
@@ -380,6 +380,7 @@ export type ComboboxProps<
> = ComboboxValueProps<TValue, TNullable, TMultiple, TTag> & {
disabled?: boolean
__demoMode?: boolean
form?: string
name?: string
}
@@ -408,6 +409,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
value: controlledValue,
defaultValue,
onChange: controlledOnChange,
form: formName,
name,
by = (a: TValue, z: TValue) => a === z,
disabled = false,
@@ -671,6 +673,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
type: 'hidden',
hidden: true,
readOnly: true,
form: formName,
name,
value,
})}
@@ -4716,6 +4716,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()
function Example() {
let [value, setValue] = useState(null)
return (
<div>
<Listbox form="my-form" value={value} onChange={setValue} name="delivery">
<Listbox.Button>Trigger</Listbox.Button>
<Listbox.Label>Pizza Delivery</Listbox.Label>
<Listbox.Options>
<Listbox.Option value="pickup">Pickup</Listbox.Option>
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
</Listbox.Options>
</Listbox>
<form
id="my-form"
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<button>Submit</button>
</form>
</div>
)
}
render(<Example />)
// 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()
@@ -343,6 +343,7 @@ export type ListboxProps<TTag extends ElementType, TType, TActualType> = 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,
})}
@@ -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 (
<div>
<RadioGroup form="my-form" value={value} onChange={setValue} name="delivery">
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<form
id="my-form"
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<button>Submit</button>
</form>
</div>
)
}
render(<Example />)
// 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 () => {
@@ -147,6 +147,7 @@ export type RadioGroupProps<TTag extends ElementType, TType> = 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<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
id = `headlessui-radiogroup-${internalId}`,
value: controlledValue,
defaultValue,
form: formName,
name,
onChange: controlledOnChange,
by = (a: TType, z: TType) => a === z,
@@ -343,6 +345,7 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
checked: value != null,
hidden: true,
readOnly: true,
form: formName,
name,
value,
})}
@@ -624,6 +624,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()
function Example() {
let [state, setState] = useState(false)
return (
<div>
<Switch.Group>
<Switch form="my-form" checked={state} onChange={setState} name="notifications" />
<Switch.Label>Enable notifications</Switch.Label>
</Switch.Group>
<form
id="my-form"
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<button>Submit</button>
</form>
</div>
)
}
render(<Example />)
// 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()
@@ -112,6 +112,7 @@ export type SwitchProps<TTag extends ElementType> = Props<
onChange?(checked: boolean): void
name?: string
value?: string
form?: string
}
>
@@ -127,6 +128,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
onChange: controlledOnChange,
name,
value,
form,
...theirProps
} = props
let groupContext = useContext(GroupContext)
@@ -193,6 +195,7 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
type: 'checkbox',
hidden: true,
readOnly: true,
form,
checked,
name,
value,
+4
View File
@@ -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
@@ -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`
<div>
<Combobox form="my-form" v-model="value" name="delivery">
<ComboboxInput />
<ComboboxButton>Trigger</ComboboxButton>
<ComboboxOptions>
<ComboboxOption value="pickup">Pickup</ComboboxOption>
<ComboboxOption value="home-delivery">Home delivery</ComboboxOption>
<ComboboxOption value="dine-in">Dine in</ComboboxOption>
</ComboboxOptions>
</Combobox>
<form id="my-form" @submit="handleSubmit">
<button>Submit</button>
</form>
</div>
`,
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()
@@ -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,
})
@@ -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`
<div>
<Listbox form="my-form" v-model="value" name="delivery">
<ListboxButton>Trigger</ListboxButton>
<ListboxOptions>
<ListboxOption value="pickup">Pickup</ListboxOption>
<ListboxOption value="home-delivery">Home delivery</ListboxOption>
<ListboxOption value="dine-in">Dine in</ListboxOption>
</ListboxOptions>
</Listbox>
<form id="my-form" @submit="handleSubmit">
<button>Submit</button>
</form>
</div>
`,
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()
@@ -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,
})
@@ -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`
<div>
<RadioGroup form="my-form" v-model="deliveryMethod" name="delivery">
<RadioGroupLabel>Pizza Delivery</RadioGroupLabel>
<RadioGroupOption value="pickup">Pickup</RadioGroupOption>
<RadioGroupOption value="home-delivery">Home delivery</RadioGroupOption>
<RadioGroupOption value="dine-in">Dine in</RadioGroupOption>
</RadioGroup>
<form id="my-form" @submit="handleSubmit">
<button>Submit</button>
</form>
</div>
`,
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()
@@ -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,
})
@@ -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`
<div>
<SwitchGroup>
<Switch form="my-form" v-model="checked" name="notifications" />
<SwitchLabel>Enable notifications</SwitchLabel>
</SwitchGroup>
<form id="my-form" @submit="handleSubmit">
<button>Submit</button>
</form>
</div>
`,
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()
@@ -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,
})