From a50be9255a29e449063ff17028f6ceb681d5fc20 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 21 Feb 2024 14:16:31 +0100 Subject: [PATCH] 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 --- packages/@headlessui-react/CHANGELOG.md | 1 + .../src/components/checkbox/checkbox.tsx | 7 ++- .../src/components/combobox/combobox.test.tsx | 42 ++++++++++++++++++ .../src/components/combobox/combobox.tsx | 1 + .../src/components/listbox/listbox.test.tsx | 41 ++++++++++++++++++ .../src/components/listbox/listbox.tsx | 7 ++- .../radio-group/radio-group.test.tsx | 35 +++++++++++++++ .../components/radio-group/radio-group.tsx | 1 + .../src/components/switch/switch.test.tsx | 33 ++++++++++++++ .../src/components/switch/switch.tsx | 7 ++- .../src/internal/form-fields.tsx | 3 ++ .../src/test-utils/scenarios.tsx | 29 +++++++++++++ packages/@headlessui-vue/CHANGELOG.md | 1 + .../src/components/combobox/combobox.test.ts | 43 +++++++++++++++++++ .../src/components/combobox/combobox.ts | 1 + .../src/components/listbox/listbox.test.tsx | 42 ++++++++++++++++++ .../src/components/listbox/listbox.ts | 1 + .../radio-group/radio-group.test.ts | 37 ++++++++++++++++ .../src/components/radio-group/radio-group.ts | 1 + .../src/components/switch/switch.test.tsx | 35 +++++++++++++++ .../src/components/switch/switch.ts | 2 + 21 files changed, 367 insertions(+), 3 deletions(-) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 14cc2f7083..1035c322ad 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `hidden` attribute to internal `` 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 `` 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 diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx index aaa7ad25e6..c67e9653ba 100644 --- a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx +++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx @@ -173,7 +173,12 @@ function CheckboxFn {name != null && ( - + )} {render({ ourProps, diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index 525f345c0f..562a33ea87 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + + Trigger + Pizza Delivery + + Pickup + Home delivery + Dine in + + + +
+ ) + } + + render() + + // 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 = [ diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 8bfd8a7fc2..7971322b05 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -907,6 +907,7 @@ function ComboboxFn {name != null && ( { 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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + Trigger + Pizza Delivery + + Pickup + Home delivery + Dine in + + + +
+ ) + } + + render() + + // 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 = [ diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 63f26e36da..129dd7408f 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -670,7 +670,12 @@ function ListboxFn< })} > {name != null && value != null && ( - + )} {render({ ourProps, diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx index 85f0fd45a6..436b2e3afe 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx @@ -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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + Pizza Delivery + Pickup + Home delivery + Dine in + + +
+ ) + } + + render() + + // 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 () => { diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx index 2ab3a7a94b..2fabaa2dc4 100644 --- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx +++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx @@ -314,6 +314,7 @@ function RadioGroupFn {name != null && ( { // 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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + + Apple + + +
+ ) + } + + render() + + // Submit the form + await click(getByText('Submit')) + + // Verify that the form has been submitted + expect(submits).toHaveBeenLastCalledWith([ + ['foo', 'bar'], // The only available field + ]) + }) }) diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 76a4ab8055..eedf7a9f7b 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -237,7 +237,12 @@ function SwitchFn( return ( <> {name != null && ( - + )} {render({ ourProps, theirProps, slot, defaultTag: DEFAULT_SWITCH_TAG, name: 'Switch' })} diff --git a/packages/@headlessui-react/src/internal/form-fields.tsx b/packages/@headlessui-react/src/internal/form-fields.tsx index 3592c57dbf..ab94d98772 100644 --- a/packages/@headlessui-react/src/internal/form-fields.tsx +++ b/packages/@headlessui-react/src/internal/form-fields.tsx @@ -31,10 +31,12 @@ export function HoistFormFields({ children }: React.PropsWithChildren<{}>) { export function FormFields({ data, form: formId, + disabled, onReset, }: { data: Record form?: string + disabled?: boolean onReset?: (e: Event) => void }) { let [form, setForm] = useState(null) @@ -61,6 +63,7 @@ export function FormFields({ hidden: true, readOnly: true, form: formId, + disabled, name, value, })} diff --git a/packages/@headlessui-react/src/test-utils/scenarios.tsx b/packages/@headlessui-react/src/test-utils/scenarios.tsx index 31ee14fb93..c576a433e1 100644 --- a/packages/@headlessui-react/src/test-utils/scenarios.tsx +++ b/packages/@headlessui-react/src/test-utils/scenarios.tsx @@ -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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + + + ) + } + + render() + + // 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 () => { diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 25b14de3f4..044bbd1639 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -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 `` ([#2929](https://github.com/tailwindlabs/headlessui/pull/2929)) - Add `hidden` attribute to internal `` component when the `Features.Hidden` feature is used ([#2955](https://github.com/tailwindlabs/headlessui/pull/2955)) - Allow setting custom `tabIndex` on the `` 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 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index a3b638f098..3222bf15a7 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -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` +
+ + + + Trigger + + Pickup + Home delivery + Dine in + + + +
+ `, + 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() diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 238c92e5c2..e3e4cc39d8 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -735,6 +735,7 @@ export let Combobox = defineComponent({ hidden: true, readOnly: true, form, + disabled, name, value, }) diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx index 5beb32b3df..b09c3c3f3a 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx +++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx @@ -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` +
+ + + Trigger + + Pickup + Home delivery + Dine in + + + +
+ `, + 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() diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index f49c4388c4..20b1ce7c2d 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -392,6 +392,7 @@ export let Listbox = defineComponent({ hidden: true, readOnly: true, form, + disabled, name, value, }) diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts index f491ae1900..42d671a46a 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts @@ -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` +
+ + + Pizza Delivery + Pickup + Home delivery + Dine in + + +
+ `, + 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() diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts index 9bf8417b36..cd63c398d7 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -262,6 +262,7 @@ export let RadioGroup = defineComponent({ hidden: true, readOnly: true, form, + disabled, name, value, }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index c4008b438b..ce39576fa5 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx @@ -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` +
+ + + + Apple + + +
+ `, + 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 + ]) + }) }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index 4746dee27e..239e0df58b 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -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, })