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:
@@ -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 })
|
||||
|
||||
@@ -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(`
|
||||
<Menu>
|
||||
<MenuButton>Trigger</MenuButton>
|
||||
<MenuItems>
|
||||
<MenuItem>Item A</MenuItem>
|
||||
<MenuItem>Item B</MenuItem>
|
||||
<MenuItem>Item C</MenuItem>
|
||||
</MenuItems>
|
||||
</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()
|
||||
renderTemplate({
|
||||
template: `
|
||||
<Menu>
|
||||
<MenuButton>Trigger</MenuButton>
|
||||
<MenuItems>
|
||||
<MenuItem as="a" @click="clickHandler">Item A</MenuItem>
|
||||
<MenuItem as="a">Item B</MenuItem>
|
||||
<MenuItem as="a">Item C</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
`,
|
||||
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(`
|
||||
<Menu>
|
||||
<MenuButton>Trigger</MenuButton>
|
||||
<MenuItems>
|
||||
<MenuItem as="a">value a</MenuItem>
|
||||
<MenuItem as="a">value b</MenuItem>
|
||||
<MenuItem as="a">value c</MenuItem>
|
||||
</MenuItems>
|
||||
</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', async () => {
|
||||
renderTemplate(`
|
||||
<Menu>
|
||||
|
||||
@@ -335,6 +335,9 @@ export const MenuItems = defineComponent({
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
|
||||
case Key.Space:
|
||||
if (api.searchQuery.value !== '') return api.search(event.key)
|
||||
// When in type ahead mode, fallthrough
|
||||
case Key.Enter:
|
||||
event.preventDefault()
|
||||
api.closeMenu()
|
||||
|
||||
Reference in New Issue
Block a user