implement type ahead mode on Menu

This will allow us to do 2 things:

- When we are in "type ahead" mode, aka search, we can use spaces to
  search. E.g.: "Account Settings" (notice the space)
- When we are not in "type ahead" mode, we can use `Space` to invoke the
  menu item. We used to only allow `Enter` and `Click`.
This commit is contained in:
Robin Malfait
2020-09-29 15:34:17 +02:00
parent 57ad598202
commit 279b021a2b
4 changed files with 246 additions and 0 deletions
@@ -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(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
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(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" onClick={clickHandler}>
Item A
</Menu.Item>
<Menu.Item as="a">Item B</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
</Menu.Items>
</Menu>
)
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(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">value a</Menu.Item>
<Menu.Item as="a">value b</Menu.Item>
<Menu.Item as="a">value c</Menu.Item>
</Menu.Items>
</Menu>
)
// 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 () => {
@@ -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 })