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:
Robin Malfait
2024-02-21 14:16:31 +01:00
committed by GitHub
parent 08baf094d2
commit a50be9255a
21 changed files with 367 additions and 3 deletions
+1
View File
@@ -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 () => {
+1
View File
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dont 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,
})