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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user