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 ( +
{ + event.preventDefault() + submits([...new FormData(event.currentTarget).entries()]) + }} + > + + Trigger + Pizza Delivery + + Pickup + Home delivery + Dine in + + + +
+ ) + } + + 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,