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 ', suppressConsoleLogs((name, Component) => { expect(() => render(Component)).toThrowError( `<${name} /> is missing a parent 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( // // {({ open }) => ( // <> // Trigger // {open && ( // // Option A // Option B // Option C // // )} // // )} // // ) // 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( // // { JSON.stringify } < /ListboxLabel> // < ListboxButton > Trigger < /ListboxButton> // < ListboxOptions > // 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( // // { JSON.stringify } < /ListboxLabel> // < ListboxButton > Trigger < /ListboxButton> // < ListboxOptions > // 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( // // { JSON.stringify } < /ListboxButton> // < ListboxOptions > // 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( // // // { JSON.stringify } // < /ListboxButton> // < ListboxOptions > // 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( // // Trigger // // {data => ( // <> // {JSON.stringify(data)} // // )} // // // ) // 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( // // Trigger // // {JSON.stringify} // // // ) // 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() }) ) })