diff --git a/packages/@headlessui-react/src/components/menu/menu.test.tsx b/packages/@headlessui-react/src/components/menu/menu.test.tsx index 5267dc3..6d5c1ef 100644 --- a/packages/@headlessui-react/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.test.tsx @@ -880,6 +880,86 @@ describe('Keyboard interactions', () => { assertNoActiveMenuItem(getMenu()) }) ) + + it( + 'should be possible to close the menu with Space when there is no active menuitem', + suppressConsoleLogs(async () => { + render( +
+ ) + + assertMenuButton(getMenuButton(), { + state: MenuButtonState.Closed, + attributes: { id: 'headlessui-menu-button-1' }, + }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Open menu + await click(getMenuButton()) + + // Verify it is open + assertMenuButton(getMenuButton(), { state: MenuButtonState.Open }) + + // Close menu + await press(Keys.Space) + + // Verify it is closed + assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed }) + assertMenu(getMenu(), { state: MenuState.Closed }) + }) + ) + + it( + 'should be possible to close the menu with Space and invoke the active menu item', + suppressConsoleLogs(async () => { + const clickHandler = jest.fn() + render( + + ) + + assertMenuButton(getMenuButton(), { + state: MenuButtonState.Closed, + attributes: { id: 'headlessui-menu-button-1' }, + }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Open menu + await click(getMenuButton()) + + // Verify it is open + assertMenuButton(getMenuButton(), { state: MenuButtonState.Open }) + + // Activate the first menu item + const items = getMenuItems() + await mouseMove(items[0]) + + // Close menu, and invoke the item + await press(Keys.Space) + + // Verify it is closed + assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Verify the "click" went through on the `a` tag + expect(clickHandler).toHaveBeenCalled() + }) + ) }) describe('`Escape` key', () => { @@ -2052,6 +2132,45 @@ describe('Keyboard interactions', () => { }) ) + it( + 'should be possible to type words with spaces', + suppressConsoleLogs(async () => { + render( + + ) + + // Focus the button + getMenuButton()?.focus() + + // Open menu + await press(Keys.ArrowUp) + + const items = getMenuItems() + + // We should be on the last item + assertMenuLinkedWithMenuItem(getMenu(), items[2]) + + // We should be able to go to the second item + await type(word('value b')) + assertMenuLinkedWithMenuItem(getMenu(), items[1]) + + // We should be able to go to the first item + await type(word('value a')) + assertMenuLinkedWithMenuItem(getMenu(), items[0]) + + // We should be able to go to the last item + await type(word('value c')) + assertMenuLinkedWithMenuItem(getMenu(), items[2]) + }) + ) + it( 'should not be possible to search for a disabled item', suppressConsoleLogs(async () => { diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 1a8d69a..5949bc3 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -397,6 +397,10 @@ const Items = forwardRefWithAs(function Items< switch (event.key) { // Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12 + case Key.Space: + if (state.searchQuery !== '') + return dispatch({ type: ActionTypes.Search, value: event.key }) + // When in type ahead mode, fallthrough case Key.Enter: event.preventDefault() dispatch({ type: ActionTypes.CloseMenu }) diff --git a/packages/@headlessui-vue/src/components/menu/menu.test.tsx b/packages/@headlessui-vue/src/components/menu/menu.test.tsx index 9976dc5..f6a1f50 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-vue/src/components/menu/menu.test.tsx @@ -1031,6 +1031,87 @@ describe('Keyboard interactions', () => { assertNoActiveMenuItem(getMenu()) }) + + it( + 'should be possible to close the menu with Space when there is no active menuitem', + suppressConsoleLogs(async () => { + renderTemplate(` + + `) + + assertMenuButton(getMenuButton(), { + state: MenuButtonState.Closed, + attributes: { id: 'headlessui-menu-button-1' }, + }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Open menu + await click(getMenuButton()) + + // Verify it is open + assertMenuButton(getMenuButton(), { state: MenuButtonState.Open }) + + // Close menu + await press(Keys.Space) + + // Verify it is closed + assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed }) + assertMenu(getMenu(), { state: MenuState.Closed }) + }) + ) + + it( + 'should be possible to close the menu with Space and invoke the active menu item', + suppressConsoleLogs(async () => { + const clickHandler = jest.fn() + renderTemplate({ + template: ` + + `, + setup: () => ({ clickHandler }), + }) + + assertMenuButton(getMenuButton(), { + state: MenuButtonState.Closed, + attributes: { id: 'headlessui-menu-button-1' }, + }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Open menu + await click(getMenuButton()) + + // Verify it is open + assertMenuButton(getMenuButton(), { state: MenuButtonState.Open }) + + // Activate the first menu item + const items = getMenuItems() + await mouseMove(items[0]) + + // Close menu, and invoke the item + await press(Keys.Space) + + // Verify it is closed + assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed }) + assertMenu(getMenu(), { state: MenuState.Closed }) + + // Verify the "click" went through on the `a` tag + expect(clickHandler).toHaveBeenCalled() + }) + ) }) describe('`Escape` key', () => { @@ -2024,6 +2105,45 @@ describe('Keyboard interactions', () => { assertMenuLinkedWithMenuItem(getMenu(), items[2]) }) + it( + 'should be possible to type words with spaces', + suppressConsoleLogs(async () => { + renderTemplate(` + + `) + + // Focus the button + getMenuButton()?.focus() + + // Open menu + await press(Keys.ArrowUp) + + const items = getMenuItems() + + // We should be on the last item + assertMenuLinkedWithMenuItem(getMenu(), items[2]) + + // We should be able to go to the second item + await type(word('value b')) + assertMenuLinkedWithMenuItem(getMenu(), items[1]) + + // We should be able to go to the first item + await type(word('value a')) + assertMenuLinkedWithMenuItem(getMenu(), items[0]) + + // We should be able to go to the last item + await type(word('value c')) + assertMenuLinkedWithMenuItem(getMenu(), items[2]) + }) + ) + it('should not be possible to search for a disabled item', async () => { renderTemplate(`