Forward disabled state to hidden inputs in form-like components (#3004)
* make hidden inputs disabled if the wrapping component is disabled * add tests to verify disabled hidden form elements * update changelog
This commit is contained in:
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
|
||||
- Attempt form submission when pressing `Enter` on `Checkbox` component ([#2962](https://github.com/tailwindlabs/headlessui/pull/2962))
|
||||
- Allow setting custom `tabIndex` on the `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))
|
||||
- Forward `disabled` state to hidden inputs in form-like components ([#3004](https://github.com/tailwindlabs/headlessui/pull/3004))
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -173,7 +173,12 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
|
||||
return (
|
||||
<>
|
||||
{name != null && (
|
||||
<FormFields data={checked ? { [name]: value || 'on' } : {}} form={form} onReset={reset} />
|
||||
<FormFields
|
||||
disabled={disabled}
|
||||
data={checked ? { [name]: value || 'on' } : {}}
|
||||
form={form}
|
||||
onReset={reset}
|
||||
/>
|
||||
)}
|
||||
{render({
|
||||
ourProps,
|
||||
|
||||
@@ -5747,6 +5747,48 @@ describe('Form compatibility', () => {
|
||||
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Combobox is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
function Example() {
|
||||
let [value, setValue] = useState('home-delivery')
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Combobox value={value} onChange={setValue} name="delivery" disabled>
|
||||
<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>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Open combobox
|
||||
await click(getComboboxButton())
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it('should be possible to submit a form with a complex value object', async () => {
|
||||
let submits = jest.fn()
|
||||
let options = [
|
||||
|
||||
@@ -907,6 +907,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
|
||||
>
|
||||
{name != null && (
|
||||
<FormFields
|
||||
disabled={disabled}
|
||||
data={value != null ? { [name]: value } : {}}
|
||||
form={form}
|
||||
onReset={reset}
|
||||
|
||||
@@ -4670,6 +4670,47 @@ describe('Form compatibility', () => {
|
||||
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Listbox is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
function Example() {
|
||||
let [value, setValue] = useState('home-delivery')
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Listbox value={value} onChange={setValue} name="delivery" disabled>
|
||||
<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>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Open listbox
|
||||
await click(getListboxButton())
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it('should be possible to submit a form with a complex value object', async () => {
|
||||
let submits = jest.fn()
|
||||
let options = [
|
||||
|
||||
@@ -670,7 +670,12 @@ function ListboxFn<
|
||||
})}
|
||||
>
|
||||
{name != null && value != null && (
|
||||
<FormFields data={{ [name]: value }} form={form} onReset={reset} />
|
||||
<FormFields
|
||||
disabled={disabled}
|
||||
data={{ [name]: value }}
|
||||
form={form}
|
||||
onReset={reset}
|
||||
/>
|
||||
)}
|
||||
{render({
|
||||
ourProps,
|
||||
|
||||
@@ -1539,6 +1539,41 @@ describe('Form compatibility', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it('should not submit the data if the RadioGroup is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
function Example() {
|
||||
let [value, setValue] = useState('home-delivery')
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<RadioGroup value={value} onChange={setValue} name="delivery" disabled>
|
||||
<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>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it(
|
||||
'should be possible to submit a form with a complex value object',
|
||||
suppressConsoleLogs(async () => {
|
||||
|
||||
@@ -314,6 +314,7 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
|
||||
<RadioGroupDataContext.Provider value={radioGroupData}>
|
||||
{name != null && (
|
||||
<FormFields
|
||||
disabled={disabled}
|
||||
data={value != null ? { [name]: value || 'on' } : {}}
|
||||
form={form}
|
||||
onReset={reset}
|
||||
|
||||
@@ -810,4 +810,37 @@ describe('Form compatibility', () => {
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([['fruit', 'apple']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Switch is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
function Example() {
|
||||
let [state, setState] = useState(true)
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Switch.Group>
|
||||
<Switch checked={state} onChange={setState} name="fruit" value="apple" disabled />
|
||||
<Switch.Label>Apple</Switch.Label>
|
||||
</Switch.Group>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -237,7 +237,12 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
|
||||
return (
|
||||
<>
|
||||
{name != null && (
|
||||
<FormFields data={checked ? { [name]: value || 'on' } : {}} form={form} onReset={reset} />
|
||||
<FormFields
|
||||
disabled={disabled}
|
||||
data={checked ? { [name]: value || 'on' } : {}}
|
||||
form={form}
|
||||
onReset={reset}
|
||||
/>
|
||||
)}
|
||||
{render({ ourProps, theirProps, slot, defaultTag: DEFAULT_SWITCH_TAG, name: 'Switch' })}
|
||||
</>
|
||||
|
||||
@@ -31,10 +31,12 @@ export function HoistFormFields({ children }: React.PropsWithChildren<{}>) {
|
||||
export function FormFields({
|
||||
data,
|
||||
form: formId,
|
||||
disabled,
|
||||
onReset,
|
||||
}: {
|
||||
data: Record<string, any>
|
||||
form?: string
|
||||
disabled?: boolean
|
||||
onReset?: (e: Event) => void
|
||||
}) {
|
||||
let [form, setForm] = useState<HTMLFormElement | null>(null)
|
||||
@@ -61,6 +63,7 @@ export function FormFields({
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
form: formId,
|
||||
disabled,
|
||||
name,
|
||||
value,
|
||||
})}
|
||||
|
||||
@@ -202,6 +202,35 @@ export function commonFormScenarios(
|
||||
expect(formDataMock.mock.calls[0][0].has('foo')).toBe(true)
|
||||
})
|
||||
|
||||
it('should not submit the data if the control is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
function Example() {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget).entries()])
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Control name="bar" disabled />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Submit the form
|
||||
await click(screen.getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it(
|
||||
'should reset the control when the form is reset',
|
||||
suppressConsoleLogs(async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Don’t override explicit `disabled` prop for components inside `<MenuItem>` ([#2929](https://github.com/tailwindlabs/headlessui/pull/2929))
|
||||
- Add `hidden` attribute to internal `<Hidden />` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955))
|
||||
- Allow setting custom `tabIndex` on the `<Switch />` component ([#2966](https://github.com/tailwindlabs/headlessui/pull/2966))
|
||||
- Forward `disabled` state to hidden inputs in form-like components ([#3004](https://github.com/tailwindlabs/headlessui/pull/3004))
|
||||
|
||||
## [1.7.19] - 2024-02-07
|
||||
|
||||
|
||||
@@ -6146,6 +6146,49 @@ describe('Form compatibility', () => {
|
||||
expect(submits).lastCalledWith([['delivery', 'pickup']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Combobox is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Combobox v-model="value" name="delivery" disabled>
|
||||
<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>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref('home-delivery')
|
||||
return {
|
||||
value,
|
||||
handleSubmit(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Open combobox
|
||||
await click(getComboboxButton())
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it('should be possible to submit a form with a complex value object', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
|
||||
@@ -735,6 +735,7 @@ export let Combobox = defineComponent({
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
form,
|
||||
disabled,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
@@ -5071,6 +5071,48 @@ describe('Form compatibility', () => {
|
||||
expect(submits).lastCalledWith([['delivery', 'pickup']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Listbox is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<Listbox v-model="value" name="delivery" disabled>
|
||||
<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>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref('home-delivery')
|
||||
return {
|
||||
value,
|
||||
handleSubmit(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Open listbox
|
||||
await click(getListboxButton())
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it('should be possible to submit a form with a complex value object', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
|
||||
@@ -392,6 +392,7 @@ export let Listbox = defineComponent({
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
form,
|
||||
disabled,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
@@ -1680,6 +1680,43 @@ describe('Form compatibility', () => {
|
||||
expect(submits).lastCalledWith([['delivery', 'pickup']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the RadioGroup is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<RadioGroup v-model="value" name="delivery" disabled>
|
||||
<RadioGroupLabel>Pizza Delivery</RadioGroupLabel>
|
||||
<RadioGroupOption value="pickup">Pickup</RadioGroupOption>
|
||||
<RadioGroupOption value="home-delivery">Home delivery</RadioGroupOption>
|
||||
<RadioGroupOption value="dine-in">Dine in</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => {
|
||||
let value = ref('home-delivery')
|
||||
return {
|
||||
value,
|
||||
handleSubmit(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
|
||||
it('should be possible to submit a form with a complex value object', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
|
||||
@@ -262,6 +262,7 @@ export let RadioGroup = defineComponent({
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
form,
|
||||
disabled,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
@@ -929,4 +929,39 @@ describe('Form compatibility', () => {
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).lastCalledWith([['fruit', 'apple']])
|
||||
})
|
||||
|
||||
it('should not submit the data if the Switch is disabled', async () => {
|
||||
let submits = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<SwitchGroup>
|
||||
<Switch v-model="checked" name="fruit" value="apple" disabled />
|
||||
<SwitchLabel>Apple</SwitchLabel>
|
||||
</SwitchGroup>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => {
|
||||
let checked = ref(true)
|
||||
return {
|
||||
checked,
|
||||
handleSubmit(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Submit the form
|
||||
await click(getByText('Submit'))
|
||||
|
||||
// Verify that the form has been submitted
|
||||
expect(submits).toHaveBeenLastCalledWith([
|
||||
['foo', 'bar'], // The only available field
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,6 +78,7 @@ export let Switch = defineComponent({
|
||||
name: { type: String, optional: true },
|
||||
value: { type: String, optional: true },
|
||||
id: { type: String, default: () => `headlessui-switch-${useId()}` },
|
||||
disabled: { type: Boolean, default: false },
|
||||
tabIndex: { type: Number, default: 0 },
|
||||
},
|
||||
inheritAttrs: false,
|
||||
@@ -172,6 +173,7 @@ export let Switch = defineComponent({
|
||||
readOnly: true,
|
||||
checked: checked.value,
|
||||
form,
|
||||
disabled: theirProps.disabled,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user