From ac859fe6b1a379c89f0074a27dba5c836a9fc303 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 25 Jul 2023 15:48:35 +0200 Subject: [PATCH] Submit form on `Enter` even if no submit-like button was found (#2613) * `requestSubmit` when a submit-like button cannot be found * add tests * update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + .../src/components/combobox/combobox.test.tsx | 48 ++++++++++++++++++ .../radio-group/radio-group.test.tsx | 38 ++++++++++++++ .../src/components/switch/switch.test.tsx | 32 ++++++++++++ packages/@headlessui-react/src/utils/form.ts | 5 ++ packages/@headlessui-vue/CHANGELOG.md | 1 + .../src/components/combobox/combobox.test.ts | 49 +++++++++++++++++++ .../radio-group/radio-group.test.ts | 36 ++++++++++++++ .../src/components/switch/switch.test.tsx | 32 ++++++++++++ packages/@headlessui-vue/src/utils/form.ts | 5 ++ 10 files changed, 247 insertions(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 9396e06d5..1ac72bf9e 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572)) - Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580)) - Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610)) +- Submit form on `Enter` even if no submit-like button was found ([#2613](https://github.com/tailwindlabs/headlessui/pull/2613)) ## [1.7.15] - 2023-06-01 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx index d9a18eb92..3642b6edb 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx @@ -2820,6 +2820,54 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledWith([['option', 'b']]) }) ) + + it( + 'should submit the form on `Enter` (when no submit button was found)', + suppressConsoleLogs(async () => { + let submits = jest.fn() + + function Example() { + let [value, setValue] = useState('b') + + return ( +
{ + // JSDom doesn't automatically submit the form but if we can + // catch an `Enter` event, we can assume it was a submit. + if (event.key === 'Enter') event.currentTarget.submit() + }} + onSubmit={(event) => { + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + Trigger + + Option A + Option B + Option C + + +
+ ) + } + + render() + + // Focus the input field + await focus(getComboboxInput()) + assertActiveElement(getComboboxInput()) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'b']]) + }) + ) }) describe('`Tab` key', () => { 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 d823ce8f1..b1feee356 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 @@ -1286,6 +1286,44 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledWith([['option', 'bob']]) }) ) + + it( + 'should submit the form on `Enter` (when no submit button was found)', + suppressConsoleLogs(async () => { + let submits = jest.fn() + + function Example() { + let [value, setValue] = useState('bob') + + return ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + Alice + Bob + Charlie + +
+ ) + } + + render() + + // Focus the RadioGroup + await press(Keys.Tab) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'bob']]) + }) + ) }) }) diff --git a/packages/@headlessui-react/src/components/switch/switch.test.tsx b/packages/@headlessui-react/src/components/switch/switch.test.tsx index bbb269c0c..1d485775d 100644 --- a/packages/@headlessui-react/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.test.tsx @@ -461,6 +461,38 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledTimes(1) expect(submits).toHaveBeenCalledWith([['option', 'on']]) }) + + it('should submit the form on `Enter` (when no submit button was found)', async () => { + let submits = jest.fn() + + function Example() { + let [value, setValue] = useState(true) + + return ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + + ) + } + + render() + + // Focus the input field + await focus(getSwitch()) + assertActiveElement(getSwitch()) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'on']]) + }) }) describe('`Tab` key', () => { diff --git a/packages/@headlessui-react/src/utils/form.ts b/packages/@headlessui-react/src/utils/form.ts index 739b7e9e3..acf5e3496 100644 --- a/packages/@headlessui-react/src/utils/form.ts +++ b/packages/@headlessui-react/src/utils/form.ts @@ -54,4 +54,9 @@ export function attemptSubmit(element: HTMLElement) { return } } + + // If we get here, then there is no submit button in the form. We can use the + // `form.requestSubmit()` function to submit the form instead. We cannot use `form.submit()` + // because then the `submit` event won't be fired and `onSubmit` listeners won't be fired. + form.requestSubmit() } diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 75da0211b..c85ba1b26 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574)) - Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580)) - Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610)) +- Submit form on `Enter` even if no submit-like button was found ([#2613](https://github.com/tailwindlabs/headlessui/pull/2613)) ## [1.7.14] - 2023-06-01 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index daf4cb17a..6d294d395 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -2941,6 +2941,55 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledWith([['option', 'b']]) }) ) + + it( + 'should submit the form on `Enter` (when no submit button was found)', + suppressConsoleLogs(async () => { + let submits = jest.fn() + + renderTemplate({ + template: html` +
+ + + Trigger + + Option A + Option B + Option C + + +
+ `, + setup() { + let value = ref('b') + return { + value, + handleKeyUp(event: KeyboardEvent) { + // JSDom doesn't automatically submit the form but if we can + // catch an `Enter` event, we can assume it was a submit. + if (event.key === 'Enter') (event.currentTarget as HTMLFormElement).submit() + }, + handleSubmit(event: SubmitEvent) { + event.preventDefault() + submits([...new FormData(event.currentTarget as HTMLFormElement).entries()]) + }, + } + }, + }) + + // Focus the input field + getComboboxInput()?.focus() + assertActiveElement(getComboboxInput()) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'b']]) + }) + ) }) describe('`Tab` key', () => { 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 1a4cc83b1..6019bc4b4 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 @@ -1488,6 +1488,42 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledTimes(1) expect(submits).toHaveBeenCalledWith([['option', 'bob']]) }) + + it('should submit the form on `Enter` (when no submit button was found)', async () => { + let submits = jest.fn() + + renderTemplate({ + template: html` +
+ + Alice + Bob + Charlie + +
+ `, + setup() { + let value = ref('bob') + return { + value, + handleSubmit(event: KeyboardEvent) { + event.preventDefault() + submits([...new FormData(event.currentTarget as HTMLFormElement).entries()]) + }, + } + }, + }) + + // Focus the RadioGroup + await press(Keys.Tab) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'bob']]) + }) }) }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.test.tsx b/packages/@headlessui-vue/src/components/switch/switch.test.tsx index a67d70834..e281e8b8b 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.test.tsx +++ b/packages/@headlessui-vue/src/components/switch/switch.test.tsx @@ -589,6 +589,38 @@ describe('Keyboard interactions', () => { expect(submits).toHaveBeenCalledTimes(1) expect(submits).toHaveBeenCalledWith([['option', 'on']]) }) + + it('should submit the form on `Enter` (when no submit button was found)', async () => { + let submits = jest.fn() + renderTemplate({ + template: html` +
+ + + `, + setup() { + let checked = ref(true) + return { + checked, + handleSubmit(event: KeyboardEvent) { + event.preventDefault() + submits([...new FormData(event.currentTarget as HTMLFormElement).entries()]) + }, + } + }, + }) + + // Focus the input field + getSwitch()?.focus() + assertActiveElement(getSwitch()) + + // Press enter (which should submit the form) + await press(Keys.Enter) + + // Verify the form was submitted + expect(submits).toHaveBeenCalledTimes(1) + expect(submits).toHaveBeenCalledWith([['option', 'on']]) + }) }) describe('`Tab` key', () => { diff --git a/packages/@headlessui-vue/src/utils/form.ts b/packages/@headlessui-vue/src/utils/form.ts index 739b7e9e3..acf5e3496 100644 --- a/packages/@headlessui-vue/src/utils/form.ts +++ b/packages/@headlessui-vue/src/utils/form.ts @@ -54,4 +54,9 @@ export function attemptSubmit(element: HTMLElement) { return } } + + // If we get here, then there is no submit button in the form. We can use the + // `form.requestSubmit()` function to submit the form instead. We cannot use `form.submit()` + // because then the `submit` event won't be fired and `onSubmit` listeners won't be fired. + form.requestSubmit() }