ea26870480
* start of combobox
* start with a copy of the Listbox
* WIP
* Add Vue Combobox
* Update Vue version of combobox
* Update tests
* Fix typescript errors in combobox test
* Fix input label
The spec says that the combobox itself is labelled directly by the associated label. The button can however be labelled by the label or itself.
* Add active descendant to combobox/input
* Add listbox role to comobox options
Right now the option list *is* just a listbox. If we were to allow other types in the future this will need to be changable
* Update tests
* move React playground to dedicated package
* add react playground script to root
* ensure we only open/close the combobox when necessary
* ensure export order is correct
* remove leftover pages directory from React package
* Only add aria controls when combobox is open
* add missing next commands
* make typescript happy
* build @headlessui/react before building playground-react
* add empty public folder
This makes vercel happy
* wip
* Add todo
* Update tests
Still more updates to do but some are blocked on implementation
* change default combobox example slightly
* ensure that we sync the input with new state
When the <Combobox value={...} /> changes, then the input should change
as well.
* only sync the value with the input in a single spot
* WIP: object value to string
* WIP
* WIP
* WIP groups
* Add static search filtering to combobox
* Move mouse leave event to combobox
* Fix use in fragments
* Update
* WIP
* make all tests pass for the combobox in React
* remove unnecessary playground item
* remove listbox wip
* only fire change event on inputs
Potentially we also have to do this for all kinds of form inputs. But
this will do for now.
* disable combobox vue tests
* Fix vue typescript errors
* Vue tests WIP
* improve combobox playgrounds a tiny bit
* ensure to lookup the correct value
* make sure that we are using a div instead of a Fragment
* expose `activeItem`
This will be similar to `yourData[activeIndex]`, but in this case the
active option's data. Can probably rename this if necessary!
* Update comments
* Port react tests to Vue
* Vue tests WIP
* WIP
* Rename activeItem to activeOption
* Move display value to input
* Update playgrounds
* Remove static filtering
* Add tests for display value
* WIP Vue Tests
* WIP
* unfocus suite
* Cleanup react accessibility assertions code
* Vue WIP
* Cleanup errors in react interactions test utils
* Update vue implementation
closer :D
* Fix searching
* Update
* Add display value stubs
* Update tests
* move `<Combobox onSearch={} />` to `<Combobox.Input onChange={} />`
* use `useLatestValue` hook
* make `onChange` explicitly required
* remove unused variables
* move `<Combobox @search="" />` to `<ComboboxInput @change="" />`
* use correct event
* use `let` for consistency
* remove unnecessary hidden check
* implement displayValue for Vue
* update playground to reflect changes
* make sure that the activeOptionIndex stays correct
* update changelog
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
4918 lines
168 KiB
TypeScript
4918 lines
168 KiB
TypeScript
import React, { createElement, useState, useEffect } from 'react'
|
|
import { render } from '@testing-library/react'
|
|
|
|
import { Combobox } from './combobox'
|
|
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
|
|
import {
|
|
click,
|
|
focus,
|
|
mouseMove,
|
|
mouseLeave,
|
|
press,
|
|
shift,
|
|
type,
|
|
word,
|
|
Keys,
|
|
MouseButton,
|
|
} from '../../test-utils/interactions'
|
|
import {
|
|
assertActiveElement,
|
|
assertActiveComboboxOption,
|
|
assertComboboxList,
|
|
assertComboboxButton,
|
|
assertComboboxButtonLinkedWithCombobox,
|
|
assertComboboxButtonLinkedWithComboboxLabel,
|
|
assertComboboxOption,
|
|
assertComboboxLabel,
|
|
assertComboboxLabelLinkedWithCombobox,
|
|
assertNoActiveComboboxOption,
|
|
assertNoSelectedComboboxOption,
|
|
getComboboxInput,
|
|
getComboboxButton,
|
|
getComboboxButtons,
|
|
getComboboxInputs,
|
|
getComboboxOptions,
|
|
getComboboxLabel,
|
|
ComboboxState,
|
|
getByText,
|
|
getComboboxes,
|
|
} from '../../test-utils/accessibility-assertions'
|
|
import { Transition } from '../transitions/transition'
|
|
|
|
let NOOP = () => {}
|
|
|
|
jest.mock('../../hooks/use-id')
|
|
|
|
beforeAll(() => {
|
|
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
|
|
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
|
|
})
|
|
|
|
afterAll(() => jest.restoreAllMocks())
|
|
|
|
describe('safeguards', () => {
|
|
it.each([
|
|
['Combobox.Button', Combobox.Button],
|
|
['Combobox.Label', Combobox.Label],
|
|
['Combobox.Options', Combobox.Options],
|
|
['Combobox.Option', Combobox.Option],
|
|
])(
|
|
'should error when we are using a <%s /> without a parent <Combobox />',
|
|
suppressConsoleLogs((name, Component) => {
|
|
// @ts-expect-error This is fine
|
|
expect(() => render(createElement(Component))).toThrowError(
|
|
`<${name} /> is missing a parent <Combobox /> component.`
|
|
)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Combobox without crashing',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
describe('Combobox', () => {
|
|
it(
|
|
'should be possible to render a Combobox using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
{({ open }) => (
|
|
<>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
{open && (
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
)}
|
|
</>
|
|
)}
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to disable a Combobox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await press(Keys.Enter, getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Combobox.Input', () => {
|
|
it(
|
|
'selecting an option puts the value into Combobox.Input when displayValue is not provided',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
|
|
await click(getComboboxOptions()[1])
|
|
|
|
expect(getComboboxInput()).toHaveValue('b')
|
|
})
|
|
)
|
|
|
|
it(
|
|
'selecting an option puts the display value into Combobox.Input when displayValue is provided',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input
|
|
onChange={NOOP}
|
|
displayValue={(str?: string) => str?.toUpperCase() ?? ''}
|
|
/>
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
|
|
await click(getComboboxOptions()[1])
|
|
|
|
expect(getComboboxInput()).toHaveValue('B')
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Combobox.Label', () => {
|
|
it(
|
|
'should be possible to render a Combobox.Label using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Label>{JSON.stringify}</Combobox.Label>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-3' },
|
|
})
|
|
assertComboboxLabel({
|
|
attributes: { id: 'headlessui-combobox-label-1' },
|
|
textContent: JSON.stringify({ open: false, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxLabel({
|
|
attributes: { id: 'headlessui-combobox-label-1' },
|
|
textContent: JSON.stringify({ open: true, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertComboboxLabelLinkedWithCombobox()
|
|
assertComboboxButtonLinkedWithComboboxLabel()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Combobox.Label using a render prop and an `as` prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Label as="p">{JSON.stringify}</Combobox.Label>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxLabel({
|
|
attributes: { id: 'headlessui-combobox-label-1' },
|
|
textContent: JSON.stringify({ open: false, disabled: false }),
|
|
tag: 'p',
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
assertComboboxLabel({
|
|
attributes: { id: 'headlessui-combobox-label-1' },
|
|
textContent: JSON.stringify({ open: true, disabled: false }),
|
|
tag: 'p',
|
|
})
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Combobox.Button', () => {
|
|
it(
|
|
'should be possible to render a Combobox.Button using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>{JSON.stringify}</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
textContent: JSON.stringify({ open: false, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
textContent: JSON.stringify({ open: true, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Combobox.Button using a render prop and an `as` prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button as="div" role="button">
|
|
{JSON.stringify}
|
|
</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
textContent: JSON.stringify({ open: false, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
textContent: JSON.stringify({ open: true, disabled: false }),
|
|
})
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Combobox.Button and a Combobox.Label and see them linked together',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Label>Label</Combobox.Label>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// TODO: Needed to make it similar to vue test implementation?
|
|
// await new Promise(requestAnimationFrame)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-3' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxButtonLinkedWithComboboxLabel()
|
|
})
|
|
)
|
|
|
|
describe('`type` attribute', () => {
|
|
it('should set the `type` to "button" by default', async () => {
|
|
render(
|
|
<Combobox value={null} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
</Combobox>
|
|
)
|
|
|
|
expect(getComboboxButton()).toHaveAttribute('type', 'button')
|
|
})
|
|
|
|
it('should not set the `type` to "button" if it already contains a `type`', async () => {
|
|
render(
|
|
<Combobox value={null} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button type="submit">Trigger</Combobox.Button>
|
|
</Combobox>
|
|
)
|
|
|
|
expect(getComboboxButton()).toHaveAttribute('type', 'submit')
|
|
})
|
|
|
|
it('should set the `type` to "button" when using the `as` prop which resolves to a "button"', async () => {
|
|
let CustomButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
|
|
<button ref={ref} {...props} />
|
|
))
|
|
|
|
render(
|
|
<Combobox value={null} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button as={CustomButton}>Trigger</Combobox.Button>
|
|
</Combobox>
|
|
)
|
|
|
|
expect(getComboboxButton()).toHaveAttribute('type', 'button')
|
|
})
|
|
|
|
it('should not set the type if the "as" prop is not a "button"', async () => {
|
|
render(
|
|
<Combobox value={null} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button as="div">Trigger</Combobox.Button>
|
|
</Combobox>
|
|
)
|
|
|
|
expect(getComboboxButton()).not.toHaveAttribute('type')
|
|
})
|
|
|
|
it('should not set the `type` to "button" when using the `as` prop which resolves to a "div"', async () => {
|
|
let CustomButton = React.forwardRef<HTMLDivElement>((props, ref) => (
|
|
<div ref={ref} {...props} />
|
|
))
|
|
|
|
render(
|
|
<Combobox value={null} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button as={CustomButton}>Trigger</Combobox.Button>
|
|
</Combobox>
|
|
)
|
|
|
|
expect(getComboboxButton()).not.toHaveAttribute('type')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Combobox.Options', () => {
|
|
it(
|
|
'should be possible to render Combobox.Options using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
{data => (
|
|
<>
|
|
<Combobox.Option value="a">{JSON.stringify(data)}</Combobox.Option>
|
|
</>
|
|
)}
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
textContent: JSON.stringify({ open: true }),
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
|
|
it('should be possible to always render the Combobox.Options if we provide it a `static` prop', () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options static>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Let's verify that the Combobox is already there
|
|
expect(getComboboxInput()).not.toBe(null)
|
|
})
|
|
|
|
it('should be possible to use a different render strategy for the Combobox.Options', async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options unmount={false}>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleHidden })
|
|
|
|
// Let's open the Combobox, to see if it is not hidden anymore
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
})
|
|
})
|
|
|
|
describe('Combobox.Option', () => {
|
|
it(
|
|
'should be possible to render a Combobox.Option using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">{JSON.stringify}</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should guarantee the order of DOM nodes when performing actions', async () => {
|
|
function Example({ hide = false }) {
|
|
return (
|
|
<>
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option 1</Combobox.Option>
|
|
{!hide && <Combobox.Option value="b">Option 2</Combobox.Option>}
|
|
<Combobox.Option value="c">Option 3</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
</>
|
|
)
|
|
}
|
|
|
|
let { rerender } = render(<Example />)
|
|
|
|
// Open the Combobox
|
|
await click(getByText('Trigger'))
|
|
|
|
rerender(<Example hide={true} />) // Remove Combobox.Option 2
|
|
rerender(<Example hide={false} />) // Re-add Combobox.Option 2
|
|
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// Focus the first item
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify that the first combobox option is active
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify that the second combobox option is active
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify that the third combobox option is active
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
})
|
|
|
|
describe('Rendering composition', () => {
|
|
it(
|
|
'should be possible to conditionally render classNames (aka className can be a function?!)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a" className={bag => JSON.stringify(bag)}>
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option value="b" disabled className={bag => JSON.stringify(bag)}>
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c" className="no-special-treatment">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open Combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// 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
|
|
assertNoActiveComboboxOption(getComboboxInput())
|
|
|
|
// 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
|
|
assertActiveComboboxOption(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
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to swap the Combobox option with a button for example',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option as="button" value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option as="button" value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option as="button" value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open Combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify options are buttons now
|
|
getComboboxOptions().forEach(option => assertComboboxOption(option, { tag: 'button' }))
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Composition', () => {
|
|
function Debug({ fn, name }: { fn: (text: string) => void; name: string }) {
|
|
useEffect(() => {
|
|
fn(`Mounting - ${name}`)
|
|
return () => {
|
|
fn(`Unmounting - ${name}`)
|
|
}
|
|
}, [fn, name])
|
|
return null
|
|
}
|
|
|
|
it(
|
|
'should be possible to wrap the Combobox.Options with a Transition component',
|
|
suppressConsoleLogs(async () => {
|
|
let orderFn = jest.fn()
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Debug name="Combobox" fn={orderFn} />
|
|
<Transition>
|
|
<Debug name="Transition" fn={orderFn} />
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">
|
|
{data => (
|
|
<>
|
|
{JSON.stringify(data)}
|
|
<Debug name="Combobox.Option" fn={orderFn} />
|
|
</>
|
|
)}
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Transition>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
await click(getComboboxButton())
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
textContent: JSON.stringify({ active: false, selected: false, disabled: false }),
|
|
})
|
|
|
|
await click(getComboboxButton())
|
|
|
|
// Verify that we tracked the `mounts` and `unmounts` in the correct order
|
|
expect(orderFn.mock.calls).toEqual([
|
|
['Mounting - Combobox'],
|
|
['Mounting - Transition'],
|
|
['Mounting - Combobox.Option'],
|
|
['Unmounting - Transition'],
|
|
['Unmounting - Combobox.Option'],
|
|
])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Keyboard interactions', () => {
|
|
describe('Button', () => {
|
|
describe('`Enter` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with Enter',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option, { selected: false }))
|
|
|
|
assertNoActiveComboboxOption()
|
|
assertNoSelectedComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with Enter when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Try to focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with Enter, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with Enter, and focus the selected option (when using the `hidden` render strategy)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options unmount={false}>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleHidden,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleHidden })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// Hover over Option A
|
|
await mouseMove(options[0])
|
|
|
|
// Verify that Option A is active
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// Verify that Option B is still selected
|
|
assertComboboxOption(options[1], { selected: true })
|
|
|
|
// Close/Hide the combobox
|
|
await press(Keys.Escape)
|
|
|
|
// Re-open the combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox 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' },
|
|
]
|
|
let selectedOption = myOptions[1]
|
|
render(
|
|
<Combobox value={selectedOption} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
{myOptions.map(myOption => (
|
|
<Combobox.Option key={myOption.id} value={myOption}>
|
|
{myOption.name}
|
|
</Combobox.Option>
|
|
))}
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Space` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with Space',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with Space when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with Space, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
})
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
})
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Space)
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option upon Space key press, when there are no non-disabled combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
})
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.Space)
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Escape` key', () => {
|
|
it(
|
|
'should be possible to close an open combobox with Escape',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Re-focus the button
|
|
getComboboxButton()?.focus()
|
|
assertActiveElement(getComboboxButton())
|
|
|
|
// Close combobox
|
|
await press(Keys.Escape)
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Verify the input is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowDown` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowDown',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// Verify that the first combobox option is active
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowDown when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowDown, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowRight` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowRight',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// Verify that the first combobox option is active
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowRight when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowRight, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
assertComboboxList({ state: ComboboxState.Visible, orientation: 'horizontal' })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowUp` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowUp and the last option should be active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// ! ALERT: The LAST option should now be active
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowUp and the last option should be active when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowUp, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowUp to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowLeft` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowLeft and the last option should be active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// ! ALERT: The LAST option should now be active
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowLeft and the last option should be active when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowLeft, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
assertComboboxList({ state: ComboboxState.Visible, orientation: 'horizontal' })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowLeft to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the button
|
|
getComboboxButton()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Input', () => {
|
|
describe('`Enter` key', () => {
|
|
it(
|
|
'should be possible to close the combobox with Enter and choose the active combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<Combobox
|
|
value={value}
|
|
onChange={value => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
|
|
// Activate the first combobox option
|
|
let options = getComboboxOptions()
|
|
await mouseMove(options[0])
|
|
|
|
// Choose option, and close combobox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Verify we got the change event
|
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
|
expect(handleChange).toHaveBeenCalledWith('a')
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Open combobox again
|
|
await click(getComboboxButton())
|
|
|
|
// Verify the active option is the previously selected one
|
|
assertActiveComboboxOption(getComboboxOptions()[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Tab` key', () => {
|
|
it(
|
|
'pressing Tab should select the active item and move to the next DOM node',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<>
|
|
<input id="before-combobox" />
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
<input id="after-combobox" />
|
|
</>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Select the 2nd option
|
|
await press(Keys.ArrowDown)
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Tab to the next DOM node
|
|
await press(Keys.Tab)
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// That the selected value was the highlighted one
|
|
expect(getComboboxInput()?.value).toBe('b')
|
|
|
|
// And focus has moved to the next element
|
|
assertActiveElement(document.querySelector('#after-combobox'))
|
|
})
|
|
)
|
|
|
|
it(
|
|
'pressing Shift+Tab should select the active item and move to the previous DOM node',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<>
|
|
<input id="before-combobox" />
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
<input id="after-combobox" />
|
|
</>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Select the 2nd option
|
|
await press(Keys.ArrowDown)
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Tab to the next DOM node
|
|
await press(shift(Keys.Tab))
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// That the selected value was the highlighted one
|
|
expect(getComboboxInput()?.value).toBe('b')
|
|
|
|
// And focus has moved to the next element
|
|
assertActiveElement(document.querySelector('#before-combobox'))
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Escape` key', () => {
|
|
it(
|
|
'should be possible to close an open combobox with Escape',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Close combobox
|
|
await press(Keys.Escape)
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowDown` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowDown',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// Verify that the first combobox option is active
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowDown when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowDown, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should NOT be able to go down again (because last option). Current implementation won't go around.
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the combobox options and skip the first disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowRight` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowRight',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// Verify that the first combobox option is active
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowRight when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowRight, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
assertComboboxList({ state: ComboboxState.Visible, orientation: 'horizontal' })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowRight to navigate the combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should NOT be able to go down again (because last option). Current implementation won't go around.
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowRight to navigate the combobox options and skip the first disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowRight to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowRight)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowUp` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowUp and the last option should be active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// ! ALERT: The LAST option should now be active
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowUp and the last option should be active when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowUp, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowUp to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to navigate up or down if there is only a single non-disabled option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertNoActiveComboboxOption()
|
|
|
|
// Going up or down should select the single available option
|
|
await press(Keys.ArrowUp)
|
|
|
|
// We should not be able to go up (because those are disabled)
|
|
await press(Keys.ArrowUp)
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should not be able to go down (because this is the last option)
|
|
await press(Keys.ArrowDown)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowUp to navigate the combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowUp)
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowUp)
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should NOT be able to go up again (because first option). Current implementation won't go around.
|
|
await press(Keys.ArrowUp)
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowLeft` key', () => {
|
|
it(
|
|
'should be possible to open the combobox with ArrowLeft and the last option should be active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
|
|
// ! ALERT: The LAST option should now be active
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox with ArrowLeft and the last option should be active when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Try to open the combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox with ArrowLeft, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
orientation: 'horizontal',
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option when there are no combobox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options />
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
assertComboboxList({ state: ComboboxState.Visible, orientation: 'horizontal' })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowLeft to navigate the combobox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} horizontal>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted, orientation: 'horizontal' })
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowLeft)
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`End` key', () => {
|
|
it(
|
|
'should be possible to use the End key to go to the last combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should have no option selected
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the last option
|
|
await press(Keys.End)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the End key to go to the last non disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should have no option selected
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the last non-disabled option
|
|
await press(Keys.End)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the End key to go to the first combobox option if that is the only non-disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.End)
|
|
|
|
let options = getComboboxOptions()
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option upon End key press, when there are no non-disabled combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.End)
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`PageDown` key', () => {
|
|
it(
|
|
'should be possible to use the PageDown key to go to the last combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be on the first option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the last option
|
|
await press(Keys.PageDown)
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageDown key to go to the last non disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Open combobox
|
|
await press(Keys.Space)
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should have nothing active
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the last non-disabled option
|
|
await press(Keys.PageDown)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageDown key to go to the first combobox option if that is the only non-disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageDown)
|
|
|
|
let options = getComboboxOptions()
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option upon PageDown key press, when there are no non-disabled combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageDown)
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Home` key', () => {
|
|
it(
|
|
'should be possible to use the Home key to go to the first combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should be able to go to the first option
|
|
await press(Keys.Home)
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the Home key to go to the first non disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
<Combobox.Option value="d">Option D</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be on the first non-disabled option
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the Home key to go to the last combobox option if that is the only non-disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option value="d">Option D</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
let options = getComboboxOptions()
|
|
assertActiveComboboxOption(options[3])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option upon Home key press, when there are no non-disabled combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`PageUp` key', () => {
|
|
it(
|
|
'should be possible to use the PageUp key to go to the first combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Focus the input
|
|
getComboboxInput()?.focus()
|
|
|
|
// Open combobox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
// We should be able to go to the first option
|
|
await press(Keys.PageUp)
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageUp key to go to the first non disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
<Combobox.Option value="d">Option D</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be on the first non-disabled option
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageUp key to go to the last combobox option if that is the only non-disabled combobox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option value="d">Option D</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
let options = getComboboxOptions()
|
|
assertActiveComboboxOption(options[3])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active combobox option upon PageUp key press, when there are no non-disabled combobox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option disabled value="a">
|
|
Option A
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="b">
|
|
Option B
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="c">
|
|
Option C
|
|
</Combobox.Option>
|
|
<Combobox.Option disabled value="d">
|
|
Option D
|
|
</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Any` key aka search', () => {
|
|
function Example(props: { people: { value: string; name: string; disabled: boolean }[] }) {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
let [query, setQuery] = useState<string>('')
|
|
let filteredPeople =
|
|
query === ''
|
|
? props.people
|
|
: props.people.filter(person => person.name.toLowerCase().includes(query.toLowerCase()))
|
|
|
|
return (
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input onChange={event => setQuery(event.target.value)} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
{filteredPeople.map(person => (
|
|
<Combobox.Option key={person.value} value={person.value} disabled={person.disabled}>
|
|
{person.name}
|
|
</Combobox.Option>
|
|
))}
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
it(
|
|
'should be possible to type a full word that has a perfect match',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Example
|
|
people={[
|
|
{ value: 'alice', name: 'alice', disabled: false },
|
|
{ value: 'bob', name: 'bob', disabled: false },
|
|
{ value: 'charlie', name: 'charlie', disabled: false },
|
|
]}
|
|
/>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify we moved focus to the input field
|
|
assertActiveElement(getComboboxInput())
|
|
let options: ReturnType<typeof getComboboxOptions>
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('bob'))
|
|
await press(Keys.Home)
|
|
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('bob')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('alice'))
|
|
await press(Keys.Home)
|
|
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('alice')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('charlie'))
|
|
await press(Keys.Home)
|
|
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('charlie')
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to type a partial of a word',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Example
|
|
people={[
|
|
{ value: 'alice', name: 'alice', disabled: false },
|
|
{ value: 'bob', name: 'bob', disabled: false },
|
|
{ value: 'charlie', name: 'charlie', disabled: false },
|
|
]}
|
|
/>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options: ReturnType<typeof getComboboxOptions>
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('bo'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('bob')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('ali'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('alice')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('char'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('charlie')
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to type words with spaces',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Example
|
|
people={[
|
|
{ value: 'alice', name: 'alice jones', disabled: false },
|
|
{ value: 'bob', name: 'bob the builder', disabled: false },
|
|
{ value: 'charlie', name: 'charlie bit me', disabled: false },
|
|
]}
|
|
/>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options: ReturnType<typeof getComboboxOptions>
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('bob t'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('bob the builder')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('alice j'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('alice jones')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('charlie b'))
|
|
await press(Keys.Home)
|
|
options = getComboboxOptions()
|
|
expect(options).toHaveLength(1)
|
|
expect(options[0]).toHaveTextContent('charlie bit me')
|
|
assertActiveComboboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to search and activate a disabled option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Example
|
|
people={[
|
|
{ value: 'alice', name: 'alice', disabled: false },
|
|
{ value: 'bob', name: 'bob', disabled: true },
|
|
{ value: 'charlie', name: 'charlie', disabled: false },
|
|
]}
|
|
/>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// We should not be able to go to the disabled option
|
|
await type(word('bo'))
|
|
await press(Keys.Home)
|
|
|
|
assertNoActiveComboboxOption()
|
|
assertNoSelectedComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should maintain activeIndex and activeOption when filtering',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Example
|
|
people={[
|
|
{ value: 'a', name: 'person a', disabled: false },
|
|
{ value: 'b', name: 'person b', disabled: false },
|
|
{ value: 'c', name: 'person c', disabled: false },
|
|
]}
|
|
/>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options: ReturnType<typeof getComboboxOptions>
|
|
|
|
await press(Keys.ArrowDown)
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Person B should be active
|
|
options = getComboboxOptions()
|
|
expect(options[1]).toHaveTextContent('person b')
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
// Filter more, remove `person a`
|
|
await type(word('person b'))
|
|
options = getComboboxOptions()
|
|
expect(options[0]).toHaveTextContent('person b')
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// Filter less, insert `person a` before `person b`
|
|
await type(word('person'))
|
|
options = getComboboxOptions()
|
|
expect(options[1]).toHaveTextContent('person b')
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Mouse interactions', () => {
|
|
it(
|
|
'should focus the Combobox.Input when we click the Combobox.Label',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Label>Label</Combobox.Label>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Ensure the button is not focused yet
|
|
assertActiveElement(document.body)
|
|
|
|
// Focus the label
|
|
await click(getComboboxLabel())
|
|
|
|
// Ensure that the actual button is focused instead
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not focus the Combobox.Input when we right click the Combobox.Label',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Label>Label</Combobox.Label>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Ensure the button is not focused yet
|
|
assertActiveElement(document.body)
|
|
|
|
// Focus the label
|
|
await click(getComboboxLabel(), MouseButton.Right)
|
|
|
|
// Ensure that the body is still active
|
|
assertActiveElement(document.body)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox on click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach(option => assertComboboxOption(option))
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox on right click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Item A</Combobox.Option>
|
|
<Combobox.Option value="b">Item B</Combobox.Option>
|
|
<Combobox.Option value="c">Item C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Try to open the combobox
|
|
await click(getComboboxButton(), MouseButton.Right)
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the combobox on click when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value={undefined} onChange={console.log} disabled>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Try to open the combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is still closed
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the combobox on click, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="b" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
assertComboboxButton({
|
|
state: ComboboxState.InvisibleUnmounted,
|
|
attributes: { id: 'headlessui-combobox-button-2' },
|
|
})
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
assertComboboxList({
|
|
state: ComboboxState.Visible,
|
|
attributes: { id: 'headlessui-combobox-options-3' },
|
|
})
|
|
assertActiveElement(getComboboxInput())
|
|
assertComboboxButtonLinkedWithCombobox()
|
|
|
|
// Verify we have combobox options
|
|
let options = getComboboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertComboboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second combobox option is active (because it is already selected)
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to close a combobox on click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="a">Option A</Combobox.Option>
|
|
<Combobox.Option value="b">Option B</Combobox.Option>
|
|
<Combobox.Option value="c">Option C</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is visible
|
|
assertComboboxButton({ state: ComboboxState.Visible })
|
|
|
|
// Click to close
|
|
await click(getComboboxButton())
|
|
|
|
// Verify it is closed
|
|
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we click outside of a closed combobox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Verify that the window is closed
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Click something that is not related to the combobox
|
|
await click(document.body)
|
|
|
|
// Should still be closed
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the combobox which should close the combobox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Click something that is not related to the combobox
|
|
await click(document.body)
|
|
|
|
// Should be closed now
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Verify the input is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the combobox on another combobox button which should close the current combobox and open the new combobox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<div>
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
</div>
|
|
)
|
|
|
|
let [button1, button2] = getComboboxButtons()
|
|
|
|
// Click the first combobox button
|
|
await click(button1)
|
|
expect(getComboboxes()).toHaveLength(1) // Only 1 combobox should be visible
|
|
|
|
// Verify that the first input is focused
|
|
assertActiveElement(getComboboxInputs()[0])
|
|
|
|
// Click the second combobox button
|
|
await click(button2)
|
|
|
|
expect(getComboboxes()).toHaveLength(1) // Only 1 combobox should be visible
|
|
|
|
// Verify that the first input is focused
|
|
assertActiveElement(getComboboxInputs()[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the combobox which should close the combobox (even if we press the combobox button)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Click the combobox button again
|
|
await click(getComboboxButton())
|
|
|
|
// Should be closed now
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
|
|
// Verify the input is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the combobox, on an element which is within a focusable element, which closes the combobox',
|
|
suppressConsoleLogs(async () => {
|
|
let focusFn = jest.fn()
|
|
render(
|
|
<div>
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} onFocus={focusFn} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
|
|
<button id="btn">
|
|
<span>Next</span>
|
|
</button>
|
|
</div>
|
|
)
|
|
|
|
// Click the combobox button
|
|
await click(getComboboxButton())
|
|
|
|
// Ensure the combobox is open
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
|
|
// Click the span inside the button
|
|
await click(getByText('Next'))
|
|
|
|
// Ensure the combobox is closed
|
|
assertComboboxList({ state: ComboboxState.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(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
// We should be able to go to the first option
|
|
await mouseMove(options[0])
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await mouseMove(options[2])
|
|
assertActiveComboboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should make a combobox option active when you move the mouse over it',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we move the mouse and the combobox option is already active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
await mouseMove(options[1])
|
|
|
|
// Nothing should be changed
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we move the mouse and the combobox option is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option disabled value="bob">
|
|
bob
|
|
</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
await mouseMove(options[1])
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to hover an option that is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option disabled value="bob">
|
|
bob
|
|
</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// Try to hover over option 1, which is disabled
|
|
await mouseMove(options[1])
|
|
|
|
// We should not have an active option now
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to mouse leave an option and make it inactive',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveComboboxOption(options[1])
|
|
|
|
await mouseLeave(options[1])
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the first option
|
|
await mouseMove(options[0])
|
|
assertActiveComboboxOption(options[0])
|
|
|
|
await mouseLeave(options[0])
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to go to the last option
|
|
await mouseMove(options[2])
|
|
assertActiveComboboxOption(options[2])
|
|
|
|
await mouseLeave(options[2])
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to mouse leave a disabled option and be a no-op',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option disabled value="bob">
|
|
bob
|
|
</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// Try to hover over option 1, which is disabled
|
|
await mouseMove(options[1])
|
|
assertNoActiveComboboxOption()
|
|
|
|
await mouseLeave(options[1])
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click a combobox option, which closes the combobox',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<Combobox
|
|
value={value}
|
|
onChange={value => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be able to click the first option
|
|
await click(options[1])
|
|
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
|
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
|
expect(handleChange).toHaveBeenCalledWith('bob')
|
|
|
|
// Verify the input is focused again
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
// Open combobox again
|
|
await click(getComboboxButton())
|
|
|
|
// Verify the active option is the previously selected one
|
|
assertActiveComboboxOption(getComboboxOptions()[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click a disabled combobox option, which is a no-op',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<Combobox
|
|
value={value}
|
|
onChange={value => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option disabled value="bob">
|
|
bob
|
|
</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should be able to click the first option
|
|
await click(options[1])
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
expect(handleChange).toHaveBeenCalledTimes(0)
|
|
|
|
// Close the combobox
|
|
await click(getComboboxButton())
|
|
|
|
// Open combobox again
|
|
await click(getComboboxButton())
|
|
|
|
// Verify the active option is non existing
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible focus a combobox option, so that it becomes active',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string | undefined>(undefined)
|
|
|
|
return (
|
|
<Combobox value={value} onChange={setValue}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option value="bob">bob</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// Verify that nothing is active yet
|
|
assertNoActiveComboboxOption()
|
|
|
|
// We should be able to focus the first option
|
|
await focus(options[1])
|
|
assertActiveComboboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to focus a combobox option which is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Combobox value="test" onChange={console.log}>
|
|
<Combobox.Input onChange={NOOP} />
|
|
<Combobox.Button>Trigger</Combobox.Button>
|
|
<Combobox.Options>
|
|
<Combobox.Option value="alice">alice</Combobox.Option>
|
|
<Combobox.Option disabled value="bob">
|
|
bob
|
|
</Combobox.Option>
|
|
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
)
|
|
|
|
// Open combobox
|
|
await click(getComboboxButton())
|
|
assertComboboxList({ state: ComboboxState.Visible })
|
|
assertActiveElement(getComboboxInput())
|
|
|
|
let options = getComboboxOptions()
|
|
|
|
// We should not be able to focus the first option
|
|
await focus(options[1])
|
|
assertNoActiveComboboxOption()
|
|
})
|
|
)
|
|
})
|