Skip to content

Commit ac859fe

Browse files
authored
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
1 parent 2eabdd4 commit ac859fe

File tree

10 files changed

+247
-0
lines changed

10 files changed

+247
-0
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
1414
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
1515
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
16+
- Submit form on `Enter` even if no submit-like button was found ([#2613](https://github.com/tailwindlabs/headlessui/pull/2613))
1617

1718
## [1.7.15] - 2023-06-01
1819

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2820,6 +2820,54 @@ describe('Keyboard interactions', () => {
28202820
expect(submits).toHaveBeenCalledWith([['option', 'b']])
28212821
})
28222822
)
2823+
2824+
it(
2825+
'should submit the form on `Enter` (when no submit button was found)',
2826+
suppressConsoleLogs(async () => {
2827+
let submits = jest.fn()
2828+
2829+
function Example() {
2830+
let [value, setValue] = useState<string>('b')
2831+
2832+
return (
2833+
<form
2834+
onKeyUp={(event) => {
2835+
// JSDom doesn't automatically submit the form but if we can
2836+
// catch an `Enter` event, we can assume it was a submit.
2837+
if (event.key === 'Enter') event.currentTarget.submit()
2838+
}}
2839+
onSubmit={(event) => {
2840+
event.preventDefault()
2841+
submits([...new FormData(event.currentTarget).entries()])
2842+
}}
2843+
>
2844+
<Combobox value={value} onChange={setValue} name="option">
2845+
<Combobox.Input onChange={NOOP} />
2846+
<Combobox.Button>Trigger</Combobox.Button>
2847+
<Combobox.Options>
2848+
<Combobox.Option value="a">Option A</Combobox.Option>
2849+
<Combobox.Option value="b">Option B</Combobox.Option>
2850+
<Combobox.Option value="c">Option C</Combobox.Option>
2851+
</Combobox.Options>
2852+
</Combobox>
2853+
</form>
2854+
)
2855+
}
2856+
2857+
render(<Example />)
2858+
2859+
// Focus the input field
2860+
await focus(getComboboxInput())
2861+
assertActiveElement(getComboboxInput())
2862+
2863+
// Press enter (which should submit the form)
2864+
await press(Keys.Enter)
2865+
2866+
// Verify the form was submitted
2867+
expect(submits).toHaveBeenCalledTimes(1)
2868+
expect(submits).toHaveBeenCalledWith([['option', 'b']])
2869+
})
2870+
)
28232871
})
28242872

28252873
describe('`Tab` key', () => {

packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,44 @@ describe('Keyboard interactions', () => {
12861286
expect(submits).toHaveBeenCalledWith([['option', 'bob']])
12871287
})
12881288
)
1289+
1290+
it(
1291+
'should submit the form on `Enter` (when no submit button was found)',
1292+
suppressConsoleLogs(async () => {
1293+
let submits = jest.fn()
1294+
1295+
function Example() {
1296+
let [value, setValue] = useState('bob')
1297+
1298+
return (
1299+
<form
1300+
onSubmit={(event) => {
1301+
event.preventDefault()
1302+
submits([...new FormData(event.currentTarget).entries()])
1303+
}}
1304+
>
1305+
<RadioGroup value={value} onChange={setValue} name="option">
1306+
<RadioGroup.Option value="alice">Alice</RadioGroup.Option>
1307+
<RadioGroup.Option value="bob">Bob</RadioGroup.Option>
1308+
<RadioGroup.Option value="charlie">Charlie</RadioGroup.Option>
1309+
</RadioGroup>
1310+
</form>
1311+
)
1312+
}
1313+
1314+
render(<Example />)
1315+
1316+
// Focus the RadioGroup
1317+
await press(Keys.Tab)
1318+
1319+
// Press enter (which should submit the form)
1320+
await press(Keys.Enter)
1321+
1322+
// Verify the form was submitted
1323+
expect(submits).toHaveBeenCalledTimes(1)
1324+
expect(submits).toHaveBeenCalledWith([['option', 'bob']])
1325+
})
1326+
)
12891327
})
12901328
})
12911329

packages/@headlessui-react/src/components/switch/switch.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,38 @@ describe('Keyboard interactions', () => {
461461
expect(submits).toHaveBeenCalledTimes(1)
462462
expect(submits).toHaveBeenCalledWith([['option', 'on']])
463463
})
464+
465+
it('should submit the form on `Enter` (when no submit button was found)', async () => {
466+
let submits = jest.fn()
467+
468+
function Example() {
469+
let [value, setValue] = useState(true)
470+
471+
return (
472+
<form
473+
onSubmit={(event) => {
474+
event.preventDefault()
475+
submits([...new FormData(event.currentTarget).entries()])
476+
}}
477+
>
478+
<Switch checked={value} onChange={setValue} name="option" />
479+
</form>
480+
)
481+
}
482+
483+
render(<Example />)
484+
485+
// Focus the input field
486+
await focus(getSwitch())
487+
assertActiveElement(getSwitch())
488+
489+
// Press enter (which should submit the form)
490+
await press(Keys.Enter)
491+
492+
// Verify the form was submitted
493+
expect(submits).toHaveBeenCalledTimes(1)
494+
expect(submits).toHaveBeenCalledWith([['option', 'on']])
495+
})
464496
})
465497

466498
describe('`Tab` key', () => {

packages/@headlessui-react/src/utils/form.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ export function attemptSubmit(element: HTMLElement) {
5454
return
5555
}
5656
}
57+
58+
// If we get here, then there is no submit button in the form. We can use the
59+
// `form.requestSubmit()` function to submit the form instead. We cannot use `form.submit()`
60+
// because then the `submit` event won't be fired and `onSubmit` listeners won't be fired.
61+
form.requestSubmit()
5762
}

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574))
1515
- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580))
1616
- Calculate `aria-expanded` purely based on the open/closed state ([#2610](https://github.com/tailwindlabs/headlessui/pull/2610))
17+
- Submit form on `Enter` even if no submit-like button was found ([#2613](https://github.com/tailwindlabs/headlessui/pull/2613))
1718

1819
## [1.7.14] - 2023-06-01
1920

packages/@headlessui-vue/src/components/combobox/combobox.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2941,6 +2941,55 @@ describe('Keyboard interactions', () => {
29412941
expect(submits).toHaveBeenCalledWith([['option', 'b']])
29422942
})
29432943
)
2944+
2945+
it(
2946+
'should submit the form on `Enter` (when no submit button was found)',
2947+
suppressConsoleLogs(async () => {
2948+
let submits = jest.fn()
2949+
2950+
renderTemplate({
2951+
template: html`
2952+
<form @submit="handleSubmit" @keyup="handleKeyUp">
2953+
<Combobox v-model="value" name="option">
2954+
<ComboboxInput />
2955+
<ComboboxButton>Trigger</ComboboxButton>
2956+
<ComboboxOptions>
2957+
<ComboboxOption value="a">Option A</ComboboxOption>
2958+
<ComboboxOption value="b">Option B</ComboboxOption>
2959+
<ComboboxOption value="c">Option C</ComboboxOption>
2960+
</ComboboxOptions>
2961+
</Combobox>
2962+
</form>
2963+
`,
2964+
setup() {
2965+
let value = ref('b')
2966+
return {
2967+
value,
2968+
handleKeyUp(event: KeyboardEvent) {
2969+
// JSDom doesn't automatically submit the form but if we can
2970+
// catch an `Enter` event, we can assume it was a submit.
2971+
if (event.key === 'Enter') (event.currentTarget as HTMLFormElement).submit()
2972+
},
2973+
handleSubmit(event: SubmitEvent) {
2974+
event.preventDefault()
2975+
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
2976+
},
2977+
}
2978+
},
2979+
})
2980+
2981+
// Focus the input field
2982+
getComboboxInput()?.focus()
2983+
assertActiveElement(getComboboxInput())
2984+
2985+
// Press enter (which should submit the form)
2986+
await press(Keys.Enter)
2987+
2988+
// Verify the form was submitted
2989+
expect(submits).toHaveBeenCalledTimes(1)
2990+
expect(submits).toHaveBeenCalledWith([['option', 'b']])
2991+
})
2992+
)
29442993
})
29452994

29462995
describe('`Tab` key', () => {

packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,42 @@ describe('Keyboard interactions', () => {
14881488
expect(submits).toHaveBeenCalledTimes(1)
14891489
expect(submits).toHaveBeenCalledWith([['option', 'bob']])
14901490
})
1491+
1492+
it('should submit the form on `Enter` (when no submit button was found)', async () => {
1493+
let submits = jest.fn()
1494+
1495+
renderTemplate({
1496+
template: html`
1497+
<form @submit="handleSubmit">
1498+
<RadioGroup v-model="value" name="option">
1499+
<RadioGroupOption value="alice">Alice</RadioGroupOption>
1500+
<RadioGroupOption value="bob">Bob</RadioGroupOption>
1501+
<RadioGroupOption value="charlie">Charlie</RadioGroupOption>
1502+
</RadioGroup>
1503+
</form>
1504+
`,
1505+
setup() {
1506+
let value = ref('bob')
1507+
return {
1508+
value,
1509+
handleSubmit(event: KeyboardEvent) {
1510+
event.preventDefault()
1511+
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
1512+
},
1513+
}
1514+
},
1515+
})
1516+
1517+
// Focus the RadioGroup
1518+
await press(Keys.Tab)
1519+
1520+
// Press enter (which should submit the form)
1521+
await press(Keys.Enter)
1522+
1523+
// Verify the form was submitted
1524+
expect(submits).toHaveBeenCalledTimes(1)
1525+
expect(submits).toHaveBeenCalledWith([['option', 'bob']])
1526+
})
14911527
})
14921528
})
14931529

packages/@headlessui-vue/src/components/switch/switch.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,38 @@ describe('Keyboard interactions', () => {
589589
expect(submits).toHaveBeenCalledTimes(1)
590590
expect(submits).toHaveBeenCalledWith([['option', 'on']])
591591
})
592+
593+
it('should submit the form on `Enter` (when no submit button was found)', async () => {
594+
let submits = jest.fn()
595+
renderTemplate({
596+
template: html`
597+
<form @submit="handleSubmit">
598+
<Switch v-model="checked" name="option" />
599+
</form>
600+
`,
601+
setup() {
602+
let checked = ref(true)
603+
return {
604+
checked,
605+
handleSubmit(event: KeyboardEvent) {
606+
event.preventDefault()
607+
submits([...new FormData(event.currentTarget as HTMLFormElement).entries()])
608+
},
609+
}
610+
},
611+
})
612+
613+
// Focus the input field
614+
getSwitch()?.focus()
615+
assertActiveElement(getSwitch())
616+
617+
// Press enter (which should submit the form)
618+
await press(Keys.Enter)
619+
620+
// Verify the form was submitted
621+
expect(submits).toHaveBeenCalledTimes(1)
622+
expect(submits).toHaveBeenCalledWith([['option', 'on']])
623+
})
592624
})
593625

594626
describe('`Tab` key', () => {

packages/@headlessui-vue/src/utils/form.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ export function attemptSubmit(element: HTMLElement) {
5454
return
5555
}
5656
}
57+
58+
// If we get here, then there is no submit button in the form. We can use the
59+
// `form.requestSubmit()` function to submit the form instead. We cannot use `form.submit()`
60+
// because then the `submit` event won't be fired and `onSubmit` listeners won't be fired.
61+
form.requestSubmit()
5762
}

0 commit comments

Comments
 (0)