Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward disabled state to hidden inputs in form-like components #3004

Merged
merged 3 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading