Files
headlessui/packages/@headlessui-react/src/components/menu/menu.test.tsx
T
Robin Malfait e9fd05e06d ensure that anchor links are still clickable
This is an issue in the Vue version (it just works in the React version)
but I added tests for them anyway.

While this solution "works" I am not 100% happy with it. Let me explain
what's happening here and why I am not that happy about it:

- For starters, the Vue `nextTick` is apparently too fast. So what we do
  is when we get the pointer up event, we will close the menu and
  re-focus the button. We ran this code in a `nextTick` so that we can
  ensure that we close the menu *after* all the click events are
  finished. However because this is too fast, the menu is already closed
  and the anchor link is already unmounted and thus not clickable
  anymore. So instead we use a double requestAnimationFrame (to mimick a
  `nextFrame` as seen in the `disposables` from the React code). This
  works, but a bit messy, oh well.
- The next reason why I am not that happy is because I can't reproduce
  it in JSDOM (Jest tests). When you *click* a link in JSDOM it doesn't
  update the `window.location.hash` or `window.location.href`. To mimick
  that behaviour I put a `@click` event on the anchor to verify that we
  actually clicked it. However this already works, even before the
  "fix". So I left a TODO in there so that we can hopefully fix the
  test, so that we _can_ reproduce this behaviour.

Fixes: #14
2020-09-28 12:30:52 +02:00

2659 lines
74 KiB
TypeScript

import React from 'react'
import { render } from '@testing-library/react'
import { Menu } from './menu'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
import {
MenuButtonState,
MenuState,
assertMenu,
assertMenuButton,
assertMenuButtonLinkedWithMenu,
assertMenuItem,
assertMenuLinkedWithMenuItem,
assertActiveElement,
assertNoActiveMenuItem,
} from '../../test-utils/accessibility-assertions'
import {
click,
focus,
hover,
mouseMove,
press,
shift,
type,
unHover,
word,
Keys,
} from '../../test-utils/interactions'
jest.mock('../../hooks/use-id')
function getMenuButton(): HTMLElement | null {
// This is just an assumption for our tests. We assume that we only have 1 button. And if we have
// more, than we assume that it is the first one.
return document.querySelector('[role="button"],button')
}
function getMenu(): HTMLElement | null {
// This is just an assumption for our tests. We assume that our menu has this role and that it is
// the first item in the DOM.
return document.querySelector('[role="menu"]')
}
function getMenuItems(): HTMLElement[] {
// This is just an assumption for our tests. We assume that all menu items have this role.
return Array.from(document.querySelectorAll('[role="menuitem"]'))
}
beforeAll(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
})
afterAll(() => jest.restoreAllMocks())
describe('Safe guards', () => {
it.each([
['Menu.Button', Menu.Button],
['Menu.Items', Menu.Items],
['Menu.Item', Menu.Item],
])(
'should error when we are using a <%s /> without a parent <Menu />',
suppressConsoleLogs((name, Component) => {
expect(() => render(React.createElement(Component))).toThrowError(
`<${name} /> is missing a parent <Menu /> component.`
)
})
)
it(
'should be possible to render a menu without crashing',
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 })
})
)
})
describe('Rendering', () => {
describe('Menu', () => {
it(
'should be possilbe to render a Menu using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
{({ open }) => (
<>
<Menu.Button>Trigger</Menu.Button>
{open && (
<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 })
await click(getMenuButton())
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Open,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Open })
})
)
})
describe('MenuButton', () => {
it(
'should be possible to render a Menu.Button using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>{JSON.stringify}</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' },
textContent: JSON.stringify({ open: false, focused: false }),
})
assertMenu(getMenu(), { state: MenuState.Closed })
await click(getMenuButton())
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Open,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
assertMenu(getMenu(), { state: MenuState.Open })
})
)
it(
'should be possible to render a Menu.Button using a render prop and an `as` prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button as="div" role="button">
{JSON.stringify}
</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' },
textContent: JSON.stringify({ open: false, focused: false }),
})
assertMenu(getMenu(), { state: MenuState.Closed })
await click(getMenuButton())
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Open,
attributes: { id: 'headlessui-menu-button-1' },
textContent: JSON.stringify({ open: true, focused: false }),
})
assertMenu(getMenu(), { state: MenuState.Open })
})
)
})
describe('MenuItems', () => {
it(
'should be possible to render Menu.Items using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
{data => (
<>
<Menu.Item as="a">{JSON.stringify(data)}</Menu.Item>
</>
)}
</Menu.Items>
</Menu>
)
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Closed,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Closed })
await click(getMenuButton())
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Open,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), {
state: MenuState.Open,
textContent: JSON.stringify({ open: true }),
})
})
)
it('should be possible to always render the MenuItems if we provide it a `static` prop', () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items static>
<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>
)
// Let's verify that the Menu is already there
expect(getMenu()).not.toBe(null)
})
})
describe('MenuItem', () => {
it(
'should be possible to render a MenuItem using a render prop',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">{JSON.stringify}</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Closed,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Closed })
await click(getMenuButton())
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Open,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), {
state: MenuState.Open,
textContent: JSON.stringify({ active: false, disabled: false }),
})
})
)
})
})
describe('Rendering composition', () => {
it(
'should be possible to conditionally render classNames (aka className can be a function?!)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" className={bag => JSON.stringify(bag)}>
Item A
</Menu.Item>
<Menu.Item as="a" disabled className={bag => JSON.stringify(bag)}>
Item B
</Menu.Item>
<Menu.Item as="a" className="no-special-treatment">
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())
const items = getMenuItems()
// Verify correct classNames
expect('' + items[0].classList).toEqual(JSON.stringify({ active: false, disabled: false }))
expect('' + items[1].classList).toEqual(JSON.stringify({ active: false, disabled: true }))
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that nothing is active
assertNoActiveMenuItem(getMenu())
// Make the first item active
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + items[0].classList).toEqual(JSON.stringify({ active: true, disabled: false }))
expect('' + items[1].classList).toEqual(JSON.stringify({ active: false, disabled: true }))
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that the first item is the active one
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// Let's go down, this should go to the third item since the second item is disabled!
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + items[0].classList).toEqual(JSON.stringify({ active: false, disabled: false }))
expect('' + items[1].classList).toEqual(JSON.stringify({ active: false, disabled: true }))
expect('' + items[2].classList).toEqual('no-special-treatment')
// Double check that the last item is the active one
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to swap the menu item with a button for example',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="button">Item A</Menu.Item>
<Menu.Item as="button">Item B</Menu.Item>
<Menu.Item as="button">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 items are buttons now
const items = getMenuItems()
items.forEach(item => assertMenuItem(item, { tag: 'button' }))
})
)
})
describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
it(
'should be possible to open the menu with Enter',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
// Verify that the first menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
assertMenu(getMenu(), { state: MenuState.Open })
assertNoActiveMenuItem(getMenu())
})
)
it(
'should focus the first non disabled menu item when opening with Enter',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should focus the first non disabled menu item when opening with Enter (jump over multiple disabled ones)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should have no active menu item upon Enter key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Closed,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to close the menu with Enter 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.Enter)
// Verify it is closed
assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed })
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
it(
'should be possible to close the menu with Enter 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 hover(items[0])
// Close menu, and invoke the item
await press(Keys.Enter)
// 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()
})
)
})
it(
'should be possible to use a button as a menu item and invoke it upon Enter',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="button" onClick={clickHandler}>
Item B
</Menu.Item>
<Menu.Item>
<button onClick={clickHandler}>Item C</button>
</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 second menu item
const items = getMenuItems()
await hover(items[1])
// Close menu, and invoke the item
await press(Keys.Enter)
// Verify it is closed
assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed })
assertMenu(getMenu(), { state: MenuState.Closed })
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(1)
// Click the menu button again
await click(getMenuButton())
// Active the last menu item
await hover(getMenuItems()[2])
// Close menu, and invoke the item
await press(Keys.Enter)
// Verify the button got "clicked"
expect(clickHandler).toHaveBeenCalledTimes(2)
})
)
describe('`Space` key', () => {
it(
'should be possible to open the menu with Space',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
assertMenu(getMenu(), { state: MenuState.Open })
assertNoActiveMenuItem(getMenu())
})
)
it(
'should focus the first non disabled menu item when opening with Space',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
const items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should focus the first non disabled menu item when opening with Space (jump over multiple disabled ones)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
const items = getMenuItems()
// Verify that the first non-disabled menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should have no active menu item upon Space key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Closed,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
assertNoActiveMenuItem(getMenu())
})
)
})
describe('`Escape` key', () => {
it(
'should be possible to close an open menu with Escape',
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>
)
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Space)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Close menu
await press(Keys.Escape)
// Verify it is closed
assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed })
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
})
describe('`Tab` key', () => {
it(
'should focus trap when we use Tab',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// Try to tab
await press(Keys.Tab)
// Verify it is still open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), { state: MenuState.Open })
})
)
it(
'should focus trap when we use Shift+Tab',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// Try to Shift+Tab
await press(shift(Keys.Tab))
// Verify it is still open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), { state: MenuState.Open })
})
)
})
describe('`ArrowDown` key', () => {
it(
'should be possible to open the menu with ArrowDown',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowDown)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
// Verify that the first menu item is active
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowDown)
assertMenu(getMenu(), { state: MenuState.Open })
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to use ArrowDown to navigate the menu items',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go down once
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go down again
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
// We should NOT be able to go down again (because last item). Current implementation won't go around.
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use ArrowDown to navigate the menu items and skip the first disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go down once
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use ArrowDown to navigate the menu items and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
})
describe('`ArrowUp` key', () => {
it(
'should be possible to open the menu with ArrowUp and the last item should be active',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
// ! ALERT: The LAST item should now be active
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should have no active menu item when there are no menu items at all',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items />
</Menu>
)
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
assertMenu(getMenu(), { state: MenuState.Open })
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to use ArrowUp to navigate the menu items and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
</Menu.Items>
</Menu>
)
assertMenuButton(getMenuButton(), {
state: MenuButtonState.Closed,
attributes: { id: 'headlessui-menu-button-1' },
})
assertMenu(getMenu(), { state: MenuState.Closed })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should not be possible to navigate up or down if there is only a single non-disabled item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[2])
// We should not be able to go up (because those are disabled)
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
// We should not be able to go down (because this is the last item)
await press(Keys.ArrowDown)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use ArrowUp to navigate the menu items',
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 })
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.ArrowUp)
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
assertMenuLinkedWithMenuItem(getMenu(), items[2])
// We should be able to go down once
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go down again
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should NOT be able to go up again (because first item). Current implementation won't go around.
await press(Keys.ArrowUp)
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
})
describe('`End` key', () => {
it(
'should be possible to use the End key to go to the last menu item',
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>
)
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last item
await press(Keys.End)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use the End key to go to the last non disabled menu item',
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" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last non-disabled item
await press(Keys.End)
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should be possible to use the End key to go to the first menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.End)
const items = getMenuItems()
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should have no active menu item upon End key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.End)
assertNoActiveMenuItem(getMenu())
})
)
})
describe('`PageDown` key', () => {
it(
'should be possible to use the PageDown key to go to the last menu item',
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>
)
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last item
await press(Keys.PageDown)
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use the PageDown key to go to the last non disabled menu item',
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" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Focus the button
getMenuButton()?.focus()
// Open menu
await press(Keys.Enter)
const items = getMenuItems()
// We should be on the first item
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last non-disabled item
await press(Keys.PageDown)
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should be possible to use the PageDown key to go to the first menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">Item A</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.PageDown)
const items = getMenuItems()
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should have no active menu item upon PageDown key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.PageDown)
assertNoActiveMenuItem(getMenu())
})
)
})
describe('`Home` key', () => {
it(
'should be possible to use the Home key to go to the first menu item',
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>
)
// 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 first item
await press(Keys.Home)
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should be possible to use the Home key to go to the first non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use the Home key to go to the last menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.Home)
const items = getMenuItems()
assertMenuLinkedWithMenuItem(getMenu(), items[3])
})
)
it(
'should have no active menu item upon Home key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.Home)
assertNoActiveMenuItem(getMenu())
})
)
})
describe('`PageUp` key', () => {
it(
'should be possible to use the PageUp key to go to the first menu item',
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>
)
// 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 first item
await press(Keys.PageUp)
assertMenuLinkedWithMenuItem(getMenu(), items[0])
})
)
it(
'should be possible to use the PageUp key to go to the first non disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a">Item C</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
// We should be on the first non-disabled item
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to use the PageUp key to go to the last menu item if that is the only non-disabled menu item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a">Item D</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.PageUp)
const items = getMenuItems()
assertMenuLinkedWithMenuItem(getMenu(), items[3])
})
)
it(
'should have no active menu item upon PageUp key press, when there are no non-disabled menu items',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" disabled>
Item A
</Menu.Item>
<Menu.Item as="a" disabled>
Item B
</Menu.Item>
<Menu.Item as="a" disabled>
Item C
</Menu.Item>
<Menu.Item as="a" disabled>
Item D
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
// We opened via click, we don't have an active item
assertNoActiveMenuItem(getMenu())
// We should not be able to go to the end
await press(Keys.PageUp)
assertNoActiveMenuItem(getMenu())
})
)
})
describe('`Any` key aka search', () => {
it(
'should be possible to type a full word that has a perfect match',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// We should be able to go to the second item
await type(word('bob'))
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go to the first item
await type(word('alice'))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last item
await type(word('charlie'))
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should be possible to type a partial of a word',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</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('bo'))
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go to the first item
await type(word('ali'))
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last item
await type(word('char'))
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should not be possible to search for a disabled item',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</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 not be able to go to the disabled item
await type(word('bo'))
// We should still be on the last item
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
})
})
describe('Mouse interactions', () => {
it(
'should be possible to open a menu on click',
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 })
assertMenu(getMenu(), {
state: MenuState.Open,
attributes: { id: 'headlessui-menu-items-2' },
})
assertMenuButtonLinkedWithMenu(getMenuButton(), getMenu())
// Verify we have menu items
const items = getMenuItems()
expect(items).toHaveLength(3)
items.forEach(item => assertMenuItem(item))
})
)
it(
'should be possible to close a menu on click',
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>
)
// Open menu
await click(getMenuButton())
// Verify it is open
assertMenuButton(getMenuButton(), { state: MenuButtonState.Open })
// Click to close
await click(getMenuButton())
// Verify it is closed
assertMenuButton(getMenuButton(), { state: MenuButtonState.Closed })
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
it('should focus the menu when you try to focus the button again (when the menu is already open)', 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>
)
// Open menu
await click(getMenuButton())
// Verify menu is focused
assertActiveElement(getMenu())
// Try to Re-focus the button
getMenuButton()?.focus()
// Verify menu is still focused
assertActiveElement(getMenu())
})
it(
'should be a no-op when we click outside of a closed menu',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Verify that the window is closed
assertMenu(getMenu(), { state: MenuState.Closed })
// Click something that is not related to the menu
await click(document.body)
// Should still be closed
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
it(
'should be possible to click outside of the menu which should close the menu',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
// Click something that is not related to the menu
await click(document.body)
// Should be closed now
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
it(
'should be possible to click outside of the menu which should close the menu (even if we press the menu button)',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
// Click the menu button again
await click(getMenuButton())
// Should be closed now
assertMenu(getMenu(), { state: MenuState.Closed })
})
)
it(
'should be possible to hover an item and make it active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// We should be able to go to the second item
await hover(items[1])
assertMenuLinkedWithMenuItem(getMenu(), items[1])
// We should be able to go to the first item
await hover(items[0])
assertMenuLinkedWithMenuItem(getMenu(), items[0])
// We should be able to go to the last item
await hover(items[2])
assertMenuLinkedWithMenuItem(getMenu(), items[2])
})
)
it(
'should make a menu item active when you move the mouse over it',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should be a no-op when we move the mouse and the menu item is already active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// We should be able to go to the second item
await mouseMove(items[1])
assertMenuLinkedWithMenuItem(getMenu(), items[1])
await mouseMove(items[1])
// Nothing should be changed
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should be a no-op when we move the mouse and the menu item is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
await mouseMove(items[1])
assertNoActiveMenuItem(getMenu())
})
)
it(
'should not be possible to hover an item that is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// Try to hover over item 1, which is disabled
await hover(items[1])
// We should not have an active item now
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to mouse leave an item and make it inactive',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// We should be able to go to the second item
await hover(items[1])
assertMenuLinkedWithMenuItem(getMenu(), items[1])
await unHover(items[1])
assertNoActiveMenuItem(getMenu())
// We should be able to go to the first item
await hover(items[0])
assertMenuLinkedWithMenuItem(getMenu(), items[0])
await unHover(items[0])
assertNoActiveMenuItem(getMenu())
// We should be able to go to the last item
await hover(items[2])
assertMenuLinkedWithMenuItem(getMenu(), items[2])
await unHover(items[2])
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to mouse leave a disabled item and be a no-op',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
const items = getMenuItems()
// Try to hover over item 1, which is disabled
await hover(items[1])
assertNoActiveMenuItem(getMenu())
await unHover(items[1])
assertNoActiveMenuItem(getMenu())
})
)
it(
'should be possible to click a menu item, which closes the menu',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" onClick={clickHandler}>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
assertMenu(getMenu(), { state: MenuState.Closed })
expect(clickHandler).toHaveBeenCalled()
})
)
it(
'should be possible to click a menu item, which closes the menu and invokes the @click handler',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="button" onClick={clickHandler}>
bob
</Menu.Item>
<Menu.Item>
<button onClick={clickHandler}>charlie</button>
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
// We should be able to click the first item
await click(getMenuItems()[1])
assertMenu(getMenu(), { state: MenuState.Closed })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(1)
// Let's re-open the window for now
await click(getMenuButton())
// Click the last item, which should close and invoke the handler
await click(getMenuItems()[2])
assertMenu(getMenu(), { state: MenuState.Closed })
// Verify the callback has been called
expect(clickHandler).toHaveBeenCalledTimes(2)
})
)
it(
'should be possible to click a disabled menu item, which is a no-op',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
const items = getMenuItems()
// We should be able to click the first item
await click(items[1])
assertMenu(getMenu(), { state: MenuState.Open })
})
)
it(
'should be possible focus a menu item, so that it becomes active',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a">bob</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
const items = getMenuItems()
// Verify that nothing is active yet
assertNoActiveMenuItem(getMenu())
// We should be able to focus the first item
await focus(items[1])
assertMenuLinkedWithMenuItem(getMenu(), items[1])
})
)
it(
'should not be possible to focus a menu item which is disabled',
suppressConsoleLogs(async () => {
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a">alice</Menu.Item>
<Menu.Item as="a" disabled>
bob
</Menu.Item>
<Menu.Item as="a">charlie</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
const items = getMenuItems()
// We should not be able to focus the first item
await focus(items[1])
assertNoActiveMenuItem(getMenu())
})
)
it(
'should not be possible to activate a disabled item',
suppressConsoleLogs(async () => {
const clickHandler = jest.fn()
render(
<Menu>
<Menu.Button>Trigger</Menu.Button>
<Menu.Items>
<Menu.Item as="a" onClick={clickHandler}>
alice
</Menu.Item>
<Menu.Item as="a" onClick={clickHandler} disabled>
bob
</Menu.Item>
<Menu.Item disabled>
<button onClick={clickHandler}>charlie</button>
</Menu.Item>
</Menu.Items>
</Menu>
)
// Open menu
await click(getMenuButton())
assertMenu(getMenu(), { state: MenuState.Open })
const items = getMenuItems()
await focus(items[0])
await click(items[1])
expect(clickHandler).not.toHaveBeenCalled()
// Activate the last item
await click(getMenuItems()[2])
expect(clickHandler).not.toHaveBeenCalled()
})
)
})