Skip to content

Commit

Permalink
Forward disabled state to hidden inputs in form-like components (#3004
Browse files Browse the repository at this point in the history
)

* make hidden inputs disabled if the wrapping component is disabled

* add tests to verify disabled hidden form elements

* update changelog
  • Loading branch information
RobinMalfait committed Feb 21, 2024
1 parent 08baf09 commit a50be92
Show file tree
Hide file tree
Showing 21 changed files with 367 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
41 changes: 41 additions & 0 deletions packages/@headlessui-react/src/components/listbox/listbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
33 changes: 33 additions & 0 deletions packages/@headlessui-react/src/components/switch/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
])
})
})
7 changes: 6 additions & 1 deletion packages/@headlessui-react/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' })}
</>
Expand Down
3 changes: 3 additions & 0 deletions packages/@headlessui-react/src/internal/form-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -61,6 +63,7 @@ export function FormFields({
hidden: true,
readOnly: true,
form: formId,
disabled,
name,
value,
})}
Expand Down
29 changes: 29 additions & 0 deletions packages/@headlessui-react/src/test-utils/scenarios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions packages/@headlessui-vue/src/components/combobox/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,7 @@ export let Combobox = defineComponent({
hidden: true,
readOnly: true,
form,
disabled,
name,
value,
})
Expand Down

0 comments on commit a50be92

Please sign in to comment.