Files
svelte-headlessui/src/lib/components/listbox/listbox.test.ts
2021-12-30 09:56:22 -10:00

3872 lines
121 KiB
TypeScript
Vendored

import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from ".";
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
import { render } from "@testing-library/svelte";
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
import {
assertActiveElement,
assertActiveListboxOption,
assertListbox,
assertListboxButton,
assertListboxButtonLinkedWithListbox,
assertListboxButtonLinkedWithListboxLabel,
assertListboxOption,
assertNoActiveListboxOption,
assertNoSelectedListboxOption,
getByText,
getListbox,
getListboxButton,
getListboxButtons,
getListboxes,
getListboxLabel,
getListboxOptions,
ListboxState,
} from "$lib/test-utils/accessibility-assertions";
import {
click,
focus,
Keys,
MouseButton,
mouseLeave,
mouseMove,
press,
shift,
type,
word,
} from "$lib/test-utils/interactions";
import { Transition } from "../transitions";
import TransitionDebug from "$lib/components/disclosure/_TransitionDebug.svelte";
import ManagedListbox from "./_ManagedListbox.svelte";
import Button from "$lib/internal/elements/Button.svelte";
import Div from "$lib/internal/elements/Div.svelte";
import Span from "$lib/internal/elements/Span.svelte";
let mockId = 0;
jest.mock('../../hooks/use-id', () => {
return {
useId: jest.fn(() => ++mockId),
}
})
beforeEach(() => mockId = 0)
beforeAll(() => {
// jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
// jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
})
afterAll(() => jest.restoreAllMocks())
describe('safeguards', () => {
it.each([
['ListboxButton', ListboxButton],
['ListboxLabel', ListboxLabel],
['ListboxOptions', ListboxOptions],
['ListboxOption', ListboxOption],
])(
'should error when we are using a <%s /> without a parent <Listbox />',
suppressConsoleLogs((name, Component) => {
expect(() => render(Component)).toThrowError(
`<${name} /> is missing a parent <Listbox /> component.`
)
})
)
it(
'should be possible to render a Listbox without crashing',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
})
describe('Rendering', () => {
describe('Listbox', () => {
// it(
// 'should be possible to render a Listbox using a render prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={undefined} onChange={console.log}>
// {({ open }) => (
// <>
// <ListboxButton>Trigger</ListboxButton>
// {open && (
// <ListboxOptions>
// <ListboxOption value="a">Option A</ListboxOption>
// <ListboxOption value="b">Option B</ListboxOption>
// <ListboxOption value="c">Option C</ListboxOption>
// </ListboxOptions>
// )}
// </>
// )}
// </Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxButton({
// state: ListboxState.Visible,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({ state: ListboxState.Visible })
// })
// )
it(
'should be possible to disable a Listbox',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
await press(Keys.Enter, getListboxButton())
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
})
describe('ListboxLabel', () => {
// it(
// 'should be possible to render a ListboxLabel using a render prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={ undefined } onChange = { console.log } >
// <ListboxLabel>{ JSON.stringify } < /ListboxLabel>
// < ListboxButton > Trigger < /ListboxButton>
// < ListboxOptions >
// <ListboxOption value="a" > Option A < /ListboxOption>
// < ListboxOption value = "b" > Option B < /ListboxOption>
// < ListboxOption value = "c" > Option C < /ListboxOption>
// < /ListboxOptions>
// < /Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-2' },
// })
// assertListboxLabel({
// attributes: { id: 'headlessui-listbox-label-1' },
// textContent: JSON.stringify({ open: false, disabled: false }),
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxLabel({
// attributes: { id: 'headlessui-listbox-label-1' },
// textContent: JSON.stringify({ open: true, disabled: false }),
// })
// assertListbox({ state: ListboxState.Visible })
// assertListboxLabelLinkedWithListbox()
// assertListboxButtonLinkedWithListboxLabel()
// })
// )
// it(
// 'should be possible to render a ListboxLabel using a render prop and an `as` prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={ undefined } onChange = { console.log } >
// <ListboxLabel as="p" > { JSON.stringify } < /ListboxLabel>
// < ListboxButton > Trigger < /ListboxButton>
// < ListboxOptions >
// <ListboxOption value="a" > Option A < /ListboxOption>
// < ListboxOption value = "b" > Option B < /ListboxOption>
// < ListboxOption value = "c" > Option C < /ListboxOption>
// < /ListboxOptions>
// < /Listbox>
// )
// assertListboxLabel({
// attributes: { id: 'headlessui-listbox-label-1' },
// textContent: JSON.stringify({ open: false, disabled: false }),
// tag: 'p',
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxLabel({
// attributes: { id: 'headlessui-listbox-label-1' },
// textContent: JSON.stringify({ open: true, disabled: false }),
// tag: 'p',
// })
// assertListbox({ state: ListboxState.Visible })
// })
// )
});
describe("ListboxButton", () => {
// it(
// 'should be possible to render a ListboxButton using a render prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={ undefined } onChange = { console.log } >
// <ListboxButton>{ JSON.stringify } < /ListboxButton>
// < ListboxOptions >
// <ListboxOption value="a" > Option A < /ListboxOption>
// < ListboxOption value = "b" > Option B < /ListboxOption>
// < ListboxOption value = "c" > Option C < /ListboxOption>
// < /ListboxOptions>
// < /Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-1' },
// textContent: JSON.stringify({ open: false, disabled: false }),
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxButton({
// state: ListboxState.Visible,
// attributes: { id: 'headlessui-listbox-button-1' },
// textContent: JSON.stringify({ open: true, disabled: false }),
// })
// assertListbox({ state: ListboxState.Visible })
// })
// )
// it(
// 'should be possible to render a ListboxButton using a render prop and an `as` prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={ undefined } onChange = { console.log } >
// <ListboxButton as="div" role = "button" >
// { JSON.stringify }
// < /ListboxButton>
// < ListboxOptions >
// <ListboxOption value="a" > Option A < /ListboxOption>
// < ListboxOption value = "b" > Option B < /ListboxOption>
// < ListboxOption value = "c" > Option C < /ListboxOption>
// < /ListboxOptions>
// < /Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-1' },
// textContent: JSON.stringify({ open: false, disabled: false }),
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxButton({
// state: ListboxState.Visible,
// attributes: { id: 'headlessui-listbox-button-1' },
// textContent: JSON.stringify({ open: true, disabled: false }),
// })
// assertListbox({ state: ListboxState.Visible })
// })
// )
it(
'should be possible to render a ListboxButton and a ListboxLabel and see them linked together',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxLabel, {}, "Label"],
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// TODO: Needed to make it similar to vue test implementation?
// await new Promise(requestAnimationFrame)
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-2' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
assertListboxButtonLinkedWithListboxLabel()
})
)
describe('`type` attribute', () => {
it('should set the `type` to "button" by default', async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: null, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
]],
]
})
expect(getListboxButton()).toHaveAttribute('type', 'button')
})
it('should not set the `type` to "button" if it already contains a `type`', async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: null, onChange: console.log }, [
[ListboxButton, { type: "submit" }, "Trigger"],
]],
]
})
expect(getListboxButton()).toHaveAttribute('type', 'submit')
})
it('should not set the type if the "as" prop is not a "button"', async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: null, onChange: console.log }, [
[ListboxButton, { as: "div" }, "Trigger"],
]],
]
})
expect(getListboxButton()).not.toHaveAttribute('type')
})
})
})
describe('ListboxOptions', () => {
// it(
// 'should be possible to render ListboxOptions using a render prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={undefined} onChange={console.log}>
// <ListboxButton>Trigger</ListboxButton>
// <ListboxOptions>
// {data => (
// <>
// <ListboxOption value="a">{JSON.stringify(data)}</ListboxOption>
// </>
// )}
// </ListboxOptions>
// </Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxButton({
// state: ListboxState.Visible,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({
// state: ListboxState.Visible,
// textContent: JSON.stringify({ open: true }),
// })
// assertActiveElement(getListbox())
// })
// )
it('should be possible to always render the ListboxOptions if we provide it a `static` prop', () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, { static: true }, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Let's verify that the Listbox is already there
expect(getListbox()).not.toBe(null)
})
it('should be possible to use a different render strategy for the ListboxOptions', async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, { unmount: false }, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListbox({ state: ListboxState.InvisibleHidden })
// Let's open the Listbox, to see if it is not hidden anymore
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
})
})
// describe('ListboxOption', () => {
// it(
// 'should be possible to render a ListboxOption using a render prop',
// suppressConsoleLogs(async () => {
// render(
// <Listbox value={undefined} onChange={console.log}>
// <ListboxButton>Trigger</ListboxButton>
// <ListboxOptions>
// <ListboxOption value="a">{JSON.stringify}</ListboxOption>
// </ListboxOptions>
// </Listbox>
// )
// assertListboxButton({
// state: ListboxState.InvisibleUnmounted,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({ state: ListboxState.InvisibleUnmounted })
// await click(getListboxButton())
// assertListboxButton({
// state: ListboxState.Visible,
// attributes: { id: 'headlessui-listbox-button-1' },
// })
// assertListbox({
// state: ListboxState.Visible,
// textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
// })
// })
// )
// })
})
describe('Rendering composition', () => {
it(
'should be possible to conditionally render classNames (aka class can be a function?!)',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a", class: (bag: any) => JSON.stringify(bag) }, "Option A"],
[ListboxOption, { value: "b", class: (bag: any) => JSON.stringify(bag), disabled: true }, "Option B"],
[ListboxOption, { value: "c", class: "no-special-treatment" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
let options = getListboxOptions()
// Verify correct classNames
expect('' + options[0].classList).toEqual(
JSON.stringify({ active: false, selected: false, disabled: false })
)
expect('' + options[1].classList).toEqual(
JSON.stringify({ active: false, selected: false, disabled: true })
)
expect('' + options[2].classList).toEqual('no-special-treatment')
// Double check that nothing is active
assertNoActiveListboxOption(getListbox())
// Make the first option active
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + options[0].classList).toEqual(
JSON.stringify({ active: true, selected: false, disabled: false })
)
expect('' + options[1].classList).toEqual(
JSON.stringify({ active: false, selected: false, disabled: true })
)
expect('' + options[2].classList).toEqual('no-special-treatment')
// Double check that the first option is the active one
assertActiveListboxOption(options[0])
// Let's go down, this should go to the third option since the second option is disabled!
await press(Keys.ArrowDown)
// Verify the classNames
expect('' + options[0].classList).toEqual(
JSON.stringify({ active: false, selected: false, disabled: false })
)
expect('' + options[1].classList).toEqual(
JSON.stringify({ active: false, selected: false, disabled: true })
)
expect('' + options[2].classList).toEqual('no-special-treatment')
// Double check that the last option is the active one
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to swap the Listbox option with a button for example',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { as: "button", value: "a" }, "Option A"],
[ListboxOption, { as: "button", value: "b" }, "Option B"],
[ListboxOption, { as: "button", value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open Listbox
await click(getListboxButton())
// Verify options are buttons now
getListboxOptions().forEach(option => assertListboxOption(option, { tag: 'button' }))
})
)
})
describe('Composition', () => {
// TODO: fix this test
it.skip(
'should be possible to wrap the ListboxOptions with a Transition component',
suppressConsoleLogs(async () => {
let orderFn = jest.fn()
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[TransitionDebug, { name: "Listbox", fn: orderFn }],
[Transition, {}, [
[TransitionDebug, { name: "Transition", fn: orderFn }],
[ListboxOptions, {}, [
[ListboxOption, { as: "button", value: "a" }, [
[TransitionDebug, { name: "ListboxOption", fn: orderFn }],
"Option A"
]]
]]
]],
]]
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
await click(getListboxButton())
assertListboxButton({
state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({
state: ListboxState.Visible,
textContent: "Option A",
})
await click(getListboxButton())
// Verify that we tracked the `mounts` and `unmounts` in the correct order
expect(orderFn.mock.calls).toEqual([
['Mounting - Listbox'],
['Mounting - Transition'],
['Mounting - ListboxOption'],
['Unmounting - Transition'],
['Unmounting - ListboxOption'],
])
})
)
})
describe('Keyboard interactions', () => {
describe('`Enter` key', () => {
it(
'should be possible to open the listbox with Enter',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, { unmount: false }, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleHidden,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleHidden })
// Focus the button
getListboxButton()?.focus()
// 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' },
]
render(
TestRenderer, {
allProps: [
[Listbox, { value: myOptions[1], onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: myOptions[0] }, "Option A"],
[ListboxOption, { value: myOptions[1] }, "Option B"],
[ListboxOption, { value: myOptions[2] }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions]
]],
]
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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 () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
// Close listbox
await press(Keys.Enter)
// Verify it is closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
})
)
it(
'should be possible to close the listbox with Enter and choose the active listbox option',
suppressConsoleLogs(async () => {
let handleChange = jest.fn()
render(
TestRenderer, {
allProps: [
[ManagedListbox, { value: undefined, onChange: (e: CustomEvent) => handleChange(e.detail) }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
await press(Keys.Enter)
// Verify it is closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('a')
// Verify the button is focused again
assertActiveElement(getListboxButton())
// Open listbox again
await click(getListboxButton())
// Verify the active option is the previously selected one
assertActiveListboxOption(getListboxOptions()[0])
})
)
})
describe('`Space` key', () => {
it(
'should be possible to open the listbox with Space',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
// 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))
assertActiveListboxOption(options[0])
})
)
it(
'should not be possible to open the listbox with Space when the button is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Try to open the listbox
await press(Keys.Space)
// 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 Space, and focus the selected option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}]
]],
]
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
})
)
it(
'should focus the first non disabled listbox option when opening with Space',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
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 Space (jump over multiple disabled ones)',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
let options = getListboxOptions()
// Verify that the first non-disabled listbox option is active
assertActiveListboxOption(options[2])
})
)
it(
'should have no active listbox option upon Space key press, when there are no non-disabled listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
assertNoActiveListboxOption()
})
)
it(
'should be possible to close the listbox with Space and choose the active listbox option',
suppressConsoleLogs(async () => {
let handleChange = jest.fn()
render(
TestRenderer, {
allProps: [
[ManagedListbox, { value: undefined, onChange: (e: CustomEvent) => handleChange(e.detail) }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
// Activate the first listbox option
let options = getListboxOptions()
await mouseMove(options[0])
// Choose option, and close listbox
await press(Keys.Space)
// Verify it is closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify we got the change event
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('a')
// Verify the button is focused again
assertActiveElement(getListboxButton())
// Open listbox again
await click(getListboxButton())
// Verify the active option is the previously selected one
assertActiveListboxOption(getListboxOptions()[0])
})
)
})
describe('`Escape` key', () => {
it(
'should be possible to close an open listbox with Escape',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Space)
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
assertListbox({
state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
})
assertActiveElement(getListbox())
assertListboxButtonLinkedWithListbox()
// Close listbox
await press(Keys.Escape)
// Verify it is closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
})
)
})
describe('`Tab` key', () => {
it(
'should focus trap when we use Tab',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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))
assertActiveListboxOption(options[0])
// Try to tab
await press(Keys.Tab)
// Verify it is still open
assertListboxButton({ state: ListboxState.Visible })
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
it(
'should focus trap when we use Shift+Tab',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// 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))
assertActiveListboxOption(options[0])
// Try to Shift+Tab
await press(shift(Keys.Tab))
// Verify it is still open
assertListboxButton({ state: ListboxState.Visible })
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
})
)
})
describe('`ArrowDown` key', () => {
it(
'should be possible to open the listbox with ArrowDown',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowDown)
// 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))
// Verify that the first listbox option is active
assertActiveListboxOption(options[0])
})
)
it(
'should not be possible to open the listbox with ArrowDown when the button is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Try to open the listbox
await press(Keys.ArrowDown)
// 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 ArrowDown, and focus the selected option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowDown)
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions]
]],
]
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowDown)
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
})
)
it(
'should be possible to use ArrowDown to navigate the listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
// We should be able to go down once
await press(Keys.ArrowDown)
assertActiveListboxOption(options[1])
// We should be able to go down again
await press(Keys.ArrowDown)
assertActiveListboxOption(options[2])
// We should NOT be able to go down again (because last option). Current implementation won't go around.
await press(Keys.ArrowDown)
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use ArrowDown to navigate the listbox options and skip the first disabled one',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[1])
// We should be able to go down once
await press(Keys.ArrowDown)
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use ArrowDown to navigate the listbox options and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
})
)
})
describe('`ArrowRight` key', () => {
it(
'should be possible to use ArrowRight to navigate the listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, horizontal: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
// We should be able to go right once
await press(Keys.ArrowRight)
assertActiveListboxOption(options[1])
// We should be able to go right again
await press(Keys.ArrowRight)
assertActiveListboxOption(options[2])
// We should NOT be able to go right again (because last option). Current implementation won't go around.
await press(Keys.ArrowRight)
assertActiveListboxOption(options[2])
})
)
})
describe('`ArrowUp` key', () => {
it(
'should be possible to open the listbox with ArrowUp and the last option should be active',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
// 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))
// ! ALERT: The LAST option should now be active
assertActiveListboxOption(options[2])
})
)
it(
'should not be possible to open the listbox with ArrowUp and the last option should be active when the button is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Try to open the listbox
await press(Keys.ArrowUp)
// 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 ArrowUp, and focus the selected option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
// 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(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}]
]],
]
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
assertNoActiveListboxOption()
})
)
it(
'should be possible to use ArrowUp to navigate the listbox options and jump to the first non-disabled one',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[0])
})
)
it(
'should not be possible to navigate up or down if there is only a single non-disabled option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
// We should not be able to go up (because those are disabled)
await press(Keys.ArrowUp)
assertActiveListboxOption(options[2])
// We should not be able to go down (because this is the last option)
await press(Keys.ArrowDown)
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use ArrowUp to navigate the listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
// 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))
assertActiveListboxOption(options[2])
// We should be able to go down once
await press(Keys.ArrowUp)
assertActiveListboxOption(options[1])
// We should be able to go down again
await press(Keys.ArrowUp)
assertActiveListboxOption(options[0])
// We should NOT be able to go up again (because first option). Current implementation won't go around.
await press(Keys.ArrowUp)
assertActiveListboxOption(options[0])
})
)
})
describe('`ArrowLeft` key', () => {
it(
'should be possible to use ArrowLeft to navigate the listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, horizontal: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
assertListbox({
state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-options-2' },
orientation: 'horizontal',
})
assertActiveElement(getListbox())
assertListboxButtonLinkedWithListbox()
// Verify we have listbox options
let options = getListboxOptions()
expect(options).toHaveLength(3)
options.forEach(option => assertListboxOption(option))
assertActiveListboxOption(options[2])
// We should be able to go left once
await press(Keys.ArrowLeft)
assertActiveListboxOption(options[1])
// We should be able to go left again
await press(Keys.ArrowLeft)
assertActiveListboxOption(options[0])
// We should NOT be able to go left again (because first option). Current implementation won't go around.
await press(Keys.ArrowLeft)
assertActiveListboxOption(options[0])
})
)
})
describe('`End` key', () => {
it(
'should be possible to use the End key to go to the last listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await press(Keys.End)
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use the End key to go to the last non disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
// We should be able to go to the last non-disabled option
await press(Keys.End)
assertActiveListboxOption(options[1])
})
)
it(
'should be possible to use the End key to go to the first listbox option if that is the only non-disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.End)
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
it(
'should have no active listbox option upon End key press, when there are no non-disabled listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.End)
assertNoActiveListboxOption()
})
)
})
describe('`PageDown` key', () => {
it(
'should be possible to use the PageDown key to go to the last listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await press(Keys.PageDown)
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use the PageDown key to go to the last non disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.Enter)
let options = getListboxOptions()
// We should be on the first option
assertActiveListboxOption(options[0])
// We should be able to go to the last non-disabled option
await press(Keys.PageDown)
assertActiveListboxOption(options[1])
})
)
it(
'should be possible to use the PageDown key to go to the first listbox option if that is the only non-disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.PageDown)
let options = getListboxOptions()
assertActiveListboxOption(options[0])
})
)
it(
'should have no active listbox option upon PageDown key press, when there are no non-disabled listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.PageDown)
assertNoActiveListboxOption()
})
)
})
describe('`Home` key', () => {
it(
'should be possible to use the Home key to go to the first listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// We should be able to go to the first option
await press(Keys.Home)
assertActiveListboxOption(options[0])
})
)
it(
'should be possible to use the Home key to go to the first non disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
[ListboxOption, { value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.Home)
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use the Home key to go to the last listbox option if that is the only non-disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.Home)
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
it(
'should have no active listbox option upon Home key press, when there are no non-disabled listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.Home)
assertNoActiveListboxOption()
})
)
})
describe('`PageUp` key', () => {
it(
'should be possible to use the PageUp key to go to the first listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// We should be able to go to the first option
await press(Keys.PageUp)
assertActiveListboxOption(options[0])
})
)
it(
'should be possible to use the PageUp key to go to the first non disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
[ListboxOption, { value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.PageUp)
let options = getListboxOptions()
// We should be on the first non-disabled option
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to use the PageUp key to go to the last listbox option if that is the only non-disabled listbox option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.PageUp)
let options = getListboxOptions()
assertActiveListboxOption(options[3])
})
)
it(
'should have no active listbox option upon PageUp key press, when there are no non-disabled listbox options',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { disabled: true, value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { disabled: true, value: "c" }, "Option C"],
[ListboxOption, { disabled: true, value: "d" }, "Option D"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// We opened via click, we don't have an active option
assertNoActiveListboxOption()
// We should not be able to go to the end
await press(Keys.PageUp)
assertNoActiveListboxOption()
})
)
})
describe('`Any` key aka search', () => {
it(
'should be possible to type a full word that has a perfect match',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "alice" }, "alice"],
[ListboxOption, { value: "bob" }, "bob"],
[ListboxOption, { value: "charlie" }, "charlie"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// We should be able to go to the second option
await type(word('bob'))
assertActiveListboxOption(options[1])
// We should be able to go to the first option
await type(word('alice'))
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await type(word('charlie'))
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to type a partial of a word',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "alice" }, "alice"],
[ListboxOption, { value: "bob" }, "bob"],
[ListboxOption, { value: "charlie" }, "charlie"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// We should be able to go to the second option
await type(word('bo'))
assertActiveListboxOption(options[1])
// We should be able to go to the first option
await type(word('ali'))
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await type(word('char'))
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to type words with spaces',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "value a"],
[ListboxOption, { value: "b" }, "value b"],
[ListboxOption, { value: "c" }, "value c"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// We should be able to go to the second option
await type(word('value b'))
assertActiveListboxOption(options[1])
// We should be able to go to the first option
await type(word('value a'))
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await type(word('value c'))
assertActiveListboxOption(options[2])
})
)
it(
'should not be possible to search for a disabled option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "alice" }, "alice"],
[ListboxOption, { disabled: true, value: "bob" }, "bob"],
[ListboxOption, { value: "charlie" }, "charlie"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// We should not be able to go to the disabled option
await type(word('bo'))
// We should still be on the last option
assertActiveListboxOption(options[2])
})
)
it(
'should be possible to search for a word (case insensitive)',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "alice" }, "alice"],
[ListboxOption, { value: "bob" }, "bob"],
[ListboxOption, { value: "charlie" }, "charlie"],
]]
]],
]
})
// Focus the button
getListboxButton()?.focus()
// Open listbox
await press(Keys.ArrowUp)
let options = getListboxOptions()
// We should be on the last option
assertActiveListboxOption(options[2])
// Search for bob in a different casing
await type(word('BO'))
// We should be on `bob`
assertActiveListboxOption(options[1])
})
)
})
})
describe('Mouse interactions', () => {
it(
'should focus the ListboxButton when we click the ListboxLabel',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxLabel, {}, "Label"],
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Ensure the button is not focused yet
assertActiveElement(document.body)
// Focus the label
await click(getListboxLabel())
// Ensure that the actual button is focused instead
assertActiveElement(getListboxButton())
})
)
it(
'should not focus the ListboxButton when we right click the ListboxLabel',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxLabel, {}, "Label"],
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Ensure the button is not focused yet
assertActiveElement(document.body)
// Focus the label
await click(getListboxLabel(), MouseButton.Right)
// Ensure that the body is still active
assertActiveElement(document.body)
})
)
it(
'should be possible to open the listbox on click',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// 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))
})
)
it(
'should not be possible to open the listbox on right click',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Try to open the listbox
await click(getListboxButton(), MouseButton.Right)
// Verify it is still closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
})
)
it(
'should not be possible to open the listbox on click when the button is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log, disabled: true }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Try to open the listbox
await click(getListboxButton())
// 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 on click, and focus the selected option',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: "b", onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Open listbox
await click(getListboxButton())
// 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 close a listbox on click',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
// Verify it is visible
assertListboxButton({ state: ListboxState.Visible })
// Click to close
await click(getListboxButton())
// Verify it is closed
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
it(
'should be a no-op when we click outside of a closed listbox',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Verify that the window is closed
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Click something that is not related to the listbox
await click(document.body)
// Should still be closed
assertListbox({ state: ListboxState.InvisibleUnmounted })
})
)
it(
'should be possible to click outside of the listbox which should close the listbox',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click something that is not related to the listbox
await click(document.body)
// Should be closed now
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
})
)
it(
'should be possible to click outside of the listbox on another listbox button which should close the current listbox and open the new listbox',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Div, {}, [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]]
]
})
let [button1, button2] = getListboxButtons()
// Click the first listbox button
await click(button1)
expect(getListboxes()).toHaveLength(1) // Only 1 listbox should be visible
// Ensure the open listbox is linked to the first button
assertListboxButtonLinkedWithListbox(button1, getListbox())
// Click the second listbox button
await click(button2)
expect(getListboxes()).toHaveLength(1) // Only 1 listbox should be visible
// Ensure the open listbox is linked to the second button
assertListboxButtonLinkedWithListbox(button2, getListbox())
})
)
it(
'should be possible to click outside of the listbox which should close the listbox (even if we press the listbox button)',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
// Click the listbox button again
await click(getListboxButton())
// Should be closed now
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Verify the button is focused again
assertActiveElement(getListboxButton())
})
)
// TODO: This test looks like it's for React-specific behavior (for some reason)
it.skip(
'should be possible to click outside of the listbox, on an element which is within a focusable element, which closes the listbox',
suppressConsoleLogs(async () => {
let focusFn = jest.fn()
render(
TestRenderer, {
allProps: [
[Div, {}, [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, { onFocus: focusFn }, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
[Button, { id: "btn" }, [
[Span, {}, "Next"]
]],
]]
]
})
// Click the listbox button
await click(getListboxButton())
// Ensure the listbox is open
assertListbox({ state: ListboxState.Visible })
// Click the span inside the button
await click(getByText('Next'))
// Ensure the listbox is closed
assertListbox({ state: ListboxState.InvisibleUnmounted })
// Ensure the outside button is focused
assertActiveElement(document.getElementById('btn'))
// Ensure that the focus button only got focus once (first click)
expect(focusFn).toHaveBeenCalledTimes(1)
})
)
it(
'should be possible to hover an option and make it active',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
// We should be able to go to the first option
await mouseMove(options[0])
assertActiveListboxOption(options[0])
// We should be able to go to the last option
await mouseMove(options[2])
assertActiveListboxOption(options[2])
})
)
it(
'should make a listbox option active when you move the mouse over it',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
})
)
it(
'should be a no-op when we move the mouse and the listbox option is already active',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
await mouseMove(options[1])
// Nothing should be changed
assertActiveListboxOption(options[1])
})
)
it(
'should be a no-op when we move the mouse and the listbox option is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
await mouseMove(options[1])
assertNoActiveListboxOption()
})
)
it(
'should not be possible to hover an option that is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
// We should not have an active option now
assertNoActiveListboxOption()
})
)
it(
'should be possible to mouse leave an option and make it inactive',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// We should be able to go to the second option
await mouseMove(options[1])
assertActiveListboxOption(options[1])
await mouseLeave(options[1])
assertNoActiveListboxOption()
// We should be able to go to the first option
await mouseMove(options[0])
assertActiveListboxOption(options[0])
await mouseLeave(options[0])
assertNoActiveListboxOption()
// We should be able to go to the last option
await mouseMove(options[2])
assertActiveListboxOption(options[2])
await mouseLeave(options[2])
assertNoActiveListboxOption()
})
)
it(
'should be possible to mouse leave a disabled option and be a no-op',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
let options = getListboxOptions()
// Try to hover over option 1, which is disabled
await mouseMove(options[1])
assertNoActiveListboxOption()
await mouseLeave(options[1])
assertNoActiveListboxOption()
})
)
it(
'should be possible to click a listbox option, which closes the listbox',
suppressConsoleLogs(async () => {
let handleChange = jest.fn()
render(
TestRenderer, {
allProps: [
[ManagedListbox, { value: undefined, onChange: (e: CustomEvent) => handleChange(e.detail) }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
assertListbox({ state: ListboxState.InvisibleUnmounted })
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('b')
// Verify the button is focused again
assertActiveElement(getListboxButton())
// Open listbox again
await click(getListboxButton())
// Verify the active option is the previously selected one
assertActiveListboxOption(getListboxOptions()[1])
})
)
it(
'should be possible to click a disabled listbox option, which is a no-op',
suppressConsoleLogs(async () => {
let handleChange = jest.fn()
render(
TestRenderer, {
allProps: [
[ManagedListbox, { value: undefined, onChange: (e: CustomEvent) => handleChange(e.detail) }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
let options = getListboxOptions()
// We should be able to click the first option
await click(options[1])
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
expect(handleChange).toHaveBeenCalledTimes(0)
// Close the listbox
await click(getListboxButton())
// Open listbox again
await click(getListboxButton())
// Verify the active option is non existing
assertNoActiveListboxOption()
})
)
it(
'should be possible focus a listbox option, so that it becomes active',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
let options = getListboxOptions()
// Verify that nothing is active yet
assertNoActiveListboxOption()
// We should be able to focus the first option
await focus(options[1])
assertActiveListboxOption(options[1])
})
)
it(
'should not be possible to focus a listbox option which is disabled',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[Listbox, { value: undefined, onChange: console.log }, [
[ListboxButton, {}, "Trigger"],
[ListboxOptions, {}, [
[ListboxOption, { value: "a" }, "Option A"],
[ListboxOption, { disabled: true, value: "b" }, "Option B"],
[ListboxOption, { value: "c" }, "Option C"],
]]
]],
]
})
// Open listbox
await click(getListboxButton())
assertListbox({ state: ListboxState.Visible })
assertActiveElement(getListbox())
let options = getListboxOptions()
// We should not be able to focus the first option
await focus(options[1])
assertNoActiveListboxOption()
})
)
})