diff --git a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
index 010e321557..acab32640c 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
@@ -12,7 +12,6 @@ import {
assertListboxLabel,
assertListboxOption,
assertNoActiveListboxOption,
- assertNoSelectedListboxOption,
getByText,
getListbox,
getListboxButton,
@@ -1490,378 +1489,6 @@ describe('Composition', () => {
describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
- it(
- 'should be possible to open the listbox with Enter',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
- Option A
- Option B
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- // Verify it is visible
- assertListboxButton({ state: ListboxState.Visible })
- assertListbox({
- state: ListboxState.Visible,
- attributes: { id: 'headlessui-listbox-options-2' },
- })
- assertActiveElement(getListbox())
- assertListboxButtonLinkedWithListbox()
-
- // Verify we have listbox options
- let options = getListboxOptions()
- expect(options).toHaveLength(3)
- options.forEach((option) => assertListboxOption(option, { selected: false }))
-
- // Verify that the first listbox option is active
- assertActiveListboxOption(options[0])
- assertNoSelectedListboxOption()
- })
- )
-
- it(
- 'should not be possible to open the listbox with Enter when the button is disabled',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)} disabled>
- Trigger
-
- Option A
- Option B
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Try to open the listbox
- await press(Keys.Enter)
-
- // Verify it is still closed
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
- })
- )
-
- it(
- 'should be possible to open the listbox with Enter, and focus the selected option',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
- Option A
- Option B
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- // Verify it is visible
- assertListboxButton({ state: ListboxState.Visible })
- assertListbox({
- state: ListboxState.Visible,
- attributes: { id: 'headlessui-listbox-options-2' },
- })
- assertActiveElement(getListbox())
- assertListboxButtonLinkedWithListbox()
-
- // Verify we have listbox options
- let options = getListboxOptions()
- expect(options).toHaveLength(3)
- options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
-
- // Verify that the second listbox option is active (because it is already selected)
- assertActiveListboxOption(options[1])
- })
- )
-
- it(
- 'should be possible to open the listbox with Enter, and focus the selected option (when using the `hidden` render strategy)',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
- Option A
- Option B
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleHidden,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleHidden })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- // Verify it is visible
- assertListboxButton({ state: ListboxState.Visible })
- assertListbox({
- state: ListboxState.Visible,
- attributes: { id: 'headlessui-listbox-options-2' },
- })
- assertActiveElement(getListbox())
- assertListboxButtonLinkedWithListbox()
-
- let options = getListboxOptions()
-
- // Hover over Option A
- await mouseMove(options[0])
-
- // Verify that Option A is active
- assertActiveListboxOption(options[0])
-
- // Verify that Option B is still selected
- assertListboxOption(options[1], { selected: true })
-
- // Close/Hide the listbox
- await press(Keys.Escape)
-
- // Re-open the listbox
- await click(getListboxButton())
-
- // Verify we have listbox options
- expect(options).toHaveLength(3)
- options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
-
- // Verify that the second listbox option is active (because it is already selected)
- assertActiveListboxOption(options[1])
- })
- )
-
- it(
- 'should be possible to open the listbox with Enter, and focus the selected option (with a list of objects)',
- suppressConsoleLogs(async () => {
- let myOptions = [
- { id: 'a', name: 'Option A' },
- { id: 'b', name: 'Option B' },
- { id: 'c', name: 'Option C' },
- ]
- let selectedOption = myOptions[1]
- render(
- console.log(x)}>
- Trigger
-
- {myOptions.map((myOption) => (
-
- {myOption.name}
-
- ))}
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- // Verify it is visible
- assertListboxButton({ state: ListboxState.Visible })
- assertListbox({
- state: ListboxState.Visible,
- attributes: { id: 'headlessui-listbox-options-2' },
- })
- assertActiveElement(getListbox())
- assertListboxButtonLinkedWithListbox()
-
- // Verify we have listbox options
- let options = getListboxOptions()
- expect(options).toHaveLength(3)
- options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
-
- // Verify that the second listbox option is active (because it is already selected)
- assertActiveListboxOption(options[1])
- })
- )
-
- it(
- 'should have no active listbox option when there are no listbox options at all',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
-
- )
-
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
- assertListbox({ state: ListboxState.Visible })
- assertActiveElement(getListbox())
-
- assertNoActiveListboxOption()
- })
- )
-
- it(
- 'should focus the first non disabled listbox option when opening with Enter',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
-
- Option A
-
- Option B
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- let options = getListboxOptions()
-
- // Verify that the first non-disabled listbox option is active
- assertActiveListboxOption(options[1])
- })
- )
-
- it(
- 'should focus the first non disabled listbox option when opening with Enter (jump over multiple disabled ones)',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
-
- Option A
-
-
- Option B
-
- Option C
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- let options = getListboxOptions()
-
- // Verify that the first non-disabled listbox option is active
- assertActiveListboxOption(options[2])
- })
- )
-
- it(
- 'should have no active listbox option upon Enter key press, when there are no non-disabled listbox options',
- suppressConsoleLogs(async () => {
- render(
- console.log(x)}>
- Trigger
-
-
- Option A
-
-
- Option B
-
-
- Option C
-
-
-
- )
-
- assertListboxButton({
- state: ListboxState.InvisibleUnmounted,
- attributes: { id: 'headlessui-listbox-button-1' },
- })
- assertListbox({ state: ListboxState.InvisibleUnmounted })
-
- // Focus the button
- await focus(getListboxButton())
-
- // Open listbox
- await press(Keys.Enter)
-
- assertNoActiveListboxOption()
- })
- )
-
it(
'should be possible to close the listbox with Enter when there is no active listboxoption',
suppressConsoleLogs(async () => {
@@ -2358,7 +1985,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
@@ -2409,7 +2036,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
@@ -2611,7 +2238,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify we have listbox options
let options = getListboxOptions()
@@ -2659,7 +2286,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify we have listbox options
let options = getListboxOptions()
@@ -2701,7 +2328,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify we have listbox options
let options = getListboxOptions()
@@ -2737,7 +2364,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify we have listbox options
let options = getListboxOptions()
@@ -2976,7 +2603,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
// Verify we have listbox options
let options = getListboxOptions()
@@ -3127,7 +2754,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
let options = getListboxOptions()
@@ -3163,7 +2790,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
let options = getListboxOptions()
@@ -3267,7 +2894,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
let options = getListboxOptions()
@@ -3303,7 +2930,7 @@ describe('Keyboard interactions', () => {
await focus(getListboxButton())
// Open listbox
- await press(Keys.Enter)
+ await press(Keys.Space)
let options = getListboxOptions()
@@ -4913,6 +4540,74 @@ describe('Form compatibility', () => {
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
})
+ it('should be possible to submit a form by pressing enter', async () => {
+ let submits = jest.fn()
+
+ function Example() {
+ let [value, setValue] = useState(null)
+ return (
+
+ )
+ }
+
+ render()
+
+ // Focus the listbox
+ await focus(getListboxButton())
+
+ // Submit the form by pressing enter
+ await press(Keys.Enter)
+
+ // Verify that the form has been submitted
+ expect(submits).toHaveBeenLastCalledWith([]) // no data
+
+ // Open listbox again
+ await click(getListboxButton())
+
+ // Choose home delivery
+ await click(getByText('Home delivery'))
+
+ // Focus the listbox
+ await focus(getListboxButton())
+
+ // Submit the form by pressing enter
+ await press(Keys.Enter)
+
+ // Verify that the form has been submitted
+ expect(submits).toHaveBeenLastCalledWith([['delivery', 'home-delivery']])
+
+ // Open listbox again
+ await click(getListboxButton())
+
+ // Choose pickup
+ await click(getByText('Pickup'))
+
+ // Focus the listbox
+ await focus(getListboxButton())
+
+ // Submit the form by pressing enter
+ await press(Keys.Enter)
+
+ // Verify that the form has been submitted
+ expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
+ })
+
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 89ce73bd9d..63f26e36da 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -54,6 +54,7 @@ import { isDisabledReactIssue7711 } from '../../utils/bugs'
import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index'
import { disposables } from '../../utils/disposables'
import { FocusableMode, isFocusableElement, sortByDomNode } from '../../utils/focus-management'
+import { attemptSubmit } from '../../utils/form'
import { match } from '../../utils/match'
import { getOwnerDocument } from '../../utils/owner'
import {
@@ -733,8 +734,11 @@ function ButtonFn(
switch (event.key) {
// Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/#keyboard-interaction-13
- case Keys.Space:
case Keys.Enter:
+ attemptSubmit(event.currentTarget)
+ break
+
+ case Keys.Space:
case Keys.ArrowDown:
event.preventDefault()
actions.openListbox()
@@ -775,6 +779,9 @@ function ButtonFn(
}
})
+ // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
+ let handleKeyPress = useEvent((event: ReactKeyboardEvent) => event.preventDefault())
+
let labelledBy = useLabelledBy([id])
let describedBy = useDescribedBy()
@@ -820,6 +827,7 @@ function ButtonFn(
disabled: data.disabled,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
+ onKeyPress: handleKeyPress,
onClick: handleClick,
},
focusProps,