a7e0f0a937
This PR simplifies the internal tests a bit. 1. Don't explicitly test if a component has a specific ID 1. Don't mock the `useId` hook if it's not necessary What we care about more is that 2 components (E.g.: `MenuButton` and `MenuItems`) are connected to each other. This is done via `id` and `aria-controls` attributes. The exact ID is not important. The main motivation for this is that every time we introduce some `useId()` hook call somewhere, the IDs will shift and it will look like some tests are broken. If we are not explicitly testing the IDs, we also don't really care about deterministic incrementing IDs in tests, so therefore we can remove some `useId` mocking. Note: some tests still have mocks like this (e.g.: `description.test.ts` & `label.test.ts`) but that's because they have some snapshot tests.
4662 lines
148 KiB
TypeScript
4662 lines
148 KiB
TypeScript
import { render, waitFor } from '@testing-library/react'
|
|
import React, { Fragment, createElement, useEffect, useState } from 'react'
|
|
import {
|
|
ListboxMode,
|
|
ListboxState,
|
|
assertActiveElement,
|
|
assertActiveListboxOption,
|
|
assertListbox,
|
|
assertListboxButton,
|
|
assertListboxButtonLinkedWithListbox,
|
|
assertListboxButtonLinkedWithListboxLabel,
|
|
assertListboxLabel,
|
|
assertListboxOption,
|
|
assertNoActiveListboxOption,
|
|
getByText,
|
|
getListbox,
|
|
getListboxButton,
|
|
getListboxButtons,
|
|
getListboxLabel,
|
|
getListboxOptions,
|
|
getListboxes,
|
|
} from '../../test-utils/accessibility-assertions'
|
|
import {
|
|
Keys,
|
|
MouseButton,
|
|
click,
|
|
focus,
|
|
mouseLeave,
|
|
mouseMove,
|
|
press,
|
|
rawClick,
|
|
shift,
|
|
type,
|
|
word,
|
|
} from '../../test-utils/interactions'
|
|
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
|
|
import { Transition } from '../transition/transition'
|
|
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from './listbox'
|
|
|
|
beforeAll(() => {
|
|
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
|
|
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
|
|
})
|
|
|
|
afterAll(() => jest.restoreAllMocks())
|
|
|
|
describe('safeguards', () => {
|
|
it.each([
|
|
['Listbox.Button', Listbox.Button],
|
|
['Listbox.Label', Listbox.Label],
|
|
['Listbox.Options', Listbox.Options],
|
|
['Listbox.Option', Listbox.Option],
|
|
])(
|
|
'should error when we are using a <%s /> without a parent <Listbox />',
|
|
suppressConsoleLogs((name, Component) => {
|
|
if (name === 'Listbox.Label') {
|
|
// @ts-expect-error This is fine
|
|
expect(() => render(createElement(Component))).toThrow(
|
|
'You used a <Label /> component, but it is not inside a relevant parent.'
|
|
)
|
|
} else {
|
|
// @ts-expect-error This is fine
|
|
expect(() => render(createElement(Component))).toThrow(
|
|
`<${name} /> is missing a parent <Listbox /> component.`
|
|
)
|
|
}
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Listbox without crashing',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
describe('Listbox', () => {
|
|
it(
|
|
'should be possible to render a Listbox using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
{({ open }) => (
|
|
<>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
{open && (
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
)}
|
|
</>
|
|
)}
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to disable a Listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await press(Keys.Enter, getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not crash in multiple mode',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox multiple name="abc">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value={{ id: 1, name: 'alice' }}>alice</Listbox.Option>
|
|
<Listbox.Option value={{ id: 2, name: 'bob' }}>bob</Listbox.Option>
|
|
<Listbox.Option value={{ id: 3, name: 'charlie' }}>charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
await click(getListboxButton())
|
|
let [alice, bob, charlie] = getListboxOptions()
|
|
|
|
await click(alice)
|
|
await click(bob)
|
|
await click(charlie)
|
|
})
|
|
)
|
|
|
|
describe('Equality', () => {
|
|
let options = [
|
|
{ id: 1, name: 'Alice' },
|
|
{ id: 2, name: 'Bob' },
|
|
{ id: 3, name: 'Charlie' },
|
|
]
|
|
|
|
it(
|
|
'should use object equality by default',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={options[1]} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
{options.map((option) => (
|
|
<Listbox.Option
|
|
key={option.id}
|
|
value={option}
|
|
className={(info) => JSON.stringify(info)}
|
|
>
|
|
{option.name}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
await click(getListboxButton())
|
|
|
|
let bob = getListboxOptions()[1]
|
|
expect(bob).toHaveAttribute(
|
|
'class',
|
|
JSON.stringify({
|
|
active: true,
|
|
focus: true,
|
|
selected: true,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to compare objects by a field',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={{ id: 2, name: 'Bob' }} onChange={(x) => console.log(x)} by="id">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
{options.map((option) => (
|
|
<Listbox.Option
|
|
key={option.id}
|
|
value={option}
|
|
className={(info) => JSON.stringify(info)}
|
|
>
|
|
{option.name}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
await click(getListboxButton())
|
|
|
|
let bob = getListboxOptions()[1]
|
|
expect(bob).toHaveAttribute(
|
|
'class',
|
|
JSON.stringify({
|
|
active: true,
|
|
focus: true,
|
|
selected: true,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to compare objects by a comparator function',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox
|
|
value={{ id: 2, name: 'Bob' }}
|
|
onChange={(x) => console.log(x)}
|
|
by={(a, z) => a.id === z.id}
|
|
>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
{options.map((option) => (
|
|
<Listbox.Option
|
|
key={option.id}
|
|
value={option}
|
|
className={(info) => JSON.stringify(info)}
|
|
>
|
|
{option.name}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
await click(getListboxButton())
|
|
|
|
let bob = getListboxOptions()[1]
|
|
expect(bob).toHaveAttribute(
|
|
'class',
|
|
JSON.stringify({
|
|
active: true,
|
|
focus: true,
|
|
selected: true,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the by prop (as a string) with a null initial value',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState(null)
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} by="id">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value={{ id: 1, name: 'alice' }}>alice</Listbox.Option>
|
|
<Listbox.Option value={{ id: 2, name: 'bob' }}>bob</Listbox.Option>
|
|
<Listbox.Option value={{ id: 3, name: 'charlie' }}>charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getListboxButton())
|
|
let [alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'false')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
|
|
await click(getListboxOptions()[2])
|
|
await click(getListboxButton())
|
|
;[alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'false')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'true')
|
|
|
|
await click(getListboxOptions()[1])
|
|
await click(getListboxButton())
|
|
;[alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
})
|
|
)
|
|
|
|
// TODO: Does this test prove anything useful?
|
|
it(
|
|
'should be possible to use the by prop (as a string) with a null listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState(null)
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} by="id">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value={null} disabled>
|
|
Please select an option
|
|
</Listbox.Option>
|
|
<Listbox.Option value={{ id: 1, name: 'alice' }}>alice</Listbox.Option>
|
|
<Listbox.Option value={{ id: 2, name: 'bob' }}>bob</Listbox.Option>
|
|
<Listbox.Option value={{ id: 3, name: 'charlie' }}>charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getListboxButton())
|
|
let [disabled, alice, bob, charlie] = getListboxOptions()
|
|
expect(disabled).toHaveAttribute('aria-selected', 'true')
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'false')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
|
|
await click(getListboxOptions()[3])
|
|
await click(getListboxButton())
|
|
;[disabled, alice, bob, charlie] = getListboxOptions()
|
|
expect(disabled).toHaveAttribute('aria-selected', 'false')
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'false')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'true')
|
|
|
|
await click(getListboxOptions()[2])
|
|
await click(getListboxButton())
|
|
;[disabled, alice, bob, charlie] = getListboxOptions()
|
|
expect(disabled).toHaveAttribute('aria-selected', 'false')
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use completely new objects while rendering (single mode)',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState({ id: 2, name: 'Bob' })
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} by="id">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value={{ id: 1, name: 'alice' }}>alice</Listbox.Option>
|
|
<Listbox.Option value={{ id: 2, name: 'bob' }}>bob</Listbox.Option>
|
|
<Listbox.Option value={{ id: 3, name: 'charlie' }}>charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getListboxButton())
|
|
let [alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
|
|
await click(getListboxOptions()[2])
|
|
await click(getListboxButton())
|
|
;[alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'false')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'true')
|
|
|
|
await click(getListboxOptions()[1])
|
|
await click(getListboxButton())
|
|
;[alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use completely new objects while rendering (multiple mode)',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState([{ id: 2, name: 'Bob' }])
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} by="id" multiple>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value={{ id: 1, name: 'alice' }}>alice</Listbox.Option>
|
|
<Listbox.Option value={{ id: 2, name: 'bob' }}>bob</Listbox.Option>
|
|
<Listbox.Option value={{ id: 3, name: 'charlie' }}>charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
await click(getListboxButton())
|
|
|
|
await click(getListboxOptions()[2])
|
|
let [alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'true')
|
|
|
|
await click(getListboxOptions()[2])
|
|
;[alice, bob, charlie] = getListboxOptions()
|
|
expect(alice).toHaveAttribute('aria-selected', 'false')
|
|
expect(bob).toHaveAttribute('aria-selected', 'true')
|
|
expect(charlie).toHaveAttribute('aria-selected', 'false')
|
|
})
|
|
)
|
|
})
|
|
|
|
it(
|
|
'null should be a valid value for the Listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Listbox.Label', () => {
|
|
it(
|
|
'should be possible to render a Listbox.Label using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Label>{(slot) => <>{JSON.stringify(slot)}</>}</Listbox.Label>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListboxLabel({ textContent: JSON.stringify({ open: false, disabled: false }) })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxLabel({ textContent: JSON.stringify({ open: true, disabled: false }) })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertListboxButtonLinkedWithListboxLabel()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Listbox.Label using a render prop and an `as` prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Label as="p">{(slot) => <>{JSON.stringify(slot)}</>}</Listbox.Label>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxLabel({
|
|
textContent: JSON.stringify({ open: false, disabled: false }),
|
|
tag: 'p',
|
|
})
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
assertListboxLabel({
|
|
textContent: JSON.stringify({ open: true, disabled: false }),
|
|
tag: 'p',
|
|
})
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Listbox.Button', () => {
|
|
it(
|
|
'should be possible to render a Listbox.Button using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>{(slot) => <>{JSON.stringify(slot)}</>}</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({
|
|
state: ListboxState.InvisibleUnmounted,
|
|
textContent: JSON.stringify({
|
|
open: false,
|
|
active: false,
|
|
disabled: false,
|
|
invalid: false,
|
|
hover: false,
|
|
focus: false,
|
|
autofocus: false,
|
|
}),
|
|
})
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({
|
|
state: ListboxState.Visible,
|
|
textContent: JSON.stringify({
|
|
open: true,
|
|
active: true,
|
|
disabled: false,
|
|
invalid: false,
|
|
hover: false,
|
|
focus: false,
|
|
autofocus: false,
|
|
}),
|
|
})
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Listbox.Button using a render prop and an `as` prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button as="div" role="button">
|
|
{(slot) => <>{JSON.stringify(slot)}</>}
|
|
</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({
|
|
state: ListboxState.InvisibleUnmounted,
|
|
textContent: JSON.stringify({
|
|
open: false,
|
|
active: false,
|
|
disabled: false,
|
|
invalid: false,
|
|
hover: false,
|
|
focus: false,
|
|
autofocus: false,
|
|
}),
|
|
})
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({
|
|
state: ListboxState.Visible,
|
|
textContent: JSON.stringify({
|
|
open: true,
|
|
active: true,
|
|
disabled: false,
|
|
invalid: false,
|
|
hover: false,
|
|
focus: false,
|
|
autofocus: false,
|
|
}),
|
|
})
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to render a Listbox.Button and a Listbox.Label and see them linked together',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Label>Label</Listbox.Label>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// TODO: Needed to make it similar to vue test implementation?
|
|
// await new Promise(requestAnimationFrame)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
assertListboxButtonLinkedWithListboxLabel()
|
|
})
|
|
)
|
|
|
|
describe('`type` attribute', () => {
|
|
it('should set the `type` to "button" by default', async () => {
|
|
render(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
</Listbox>
|
|
)
|
|
|
|
expect(getListboxButton()).toHaveAttribute('type', 'button')
|
|
})
|
|
|
|
it('should not set the `type` to "button" if it already contains a `type`', async () => {
|
|
render(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button type="submit">Trigger</Listbox.Button>
|
|
</Listbox>
|
|
)
|
|
|
|
expect(getListboxButton()).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(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button as={CustomButton}>Trigger</Listbox.Button>
|
|
</Listbox>
|
|
)
|
|
|
|
expect(getListboxButton()).toHaveAttribute('type', 'button')
|
|
})
|
|
|
|
it('should not set the type if the "as" prop is not a "button"', async () => {
|
|
render(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button as="div">Trigger</Listbox.Button>
|
|
</Listbox>
|
|
)
|
|
|
|
expect(getListboxButton()).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(
|
|
<Listbox value={null} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button as={CustomButton}>Trigger</Listbox.Button>
|
|
</Listbox>
|
|
)
|
|
|
|
expect(getListboxButton()).not.toHaveAttribute('type')
|
|
})
|
|
})
|
|
|
|
it(
|
|
'should be possible to render a ListboxButton using as={Fragment}',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox>
|
|
<ListboxButton as={Fragment}>
|
|
<button>Toggle</button>
|
|
</ListboxButton>
|
|
<ListboxOptions>
|
|
<ListboxOption value="a">Option A</ListboxOption>
|
|
<ListboxOption value="b">Option B</ListboxOption>
|
|
<ListboxOption value="c">Option C</ListboxOption>
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Listbox.Options', () => {
|
|
it(
|
|
'should be possible to render Listbox.Options using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
{(data) => (
|
|
<>
|
|
<Listbox.Option value="a">{JSON.stringify(data)}</Listbox.Option>
|
|
</>
|
|
)}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible, textContent: JSON.stringify({ open: true }) })
|
|
assertActiveElement(getListbox())
|
|
})
|
|
)
|
|
|
|
it('should be possible to always render the Listbox.Options if we provide it a `static` prop', () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options static>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Let's verify that the Listbox is already there
|
|
expect(getListbox()).not.toBe(null)
|
|
})
|
|
|
|
it('should be possible to use a different render strategy for the Listbox.Options', async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options unmount={false}>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListbox({ state: ListboxState.InvisibleHidden })
|
|
|
|
// Let's open the Listbox, to see if it is not hidden anymore
|
|
await click(getListboxButton())
|
|
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
})
|
|
|
|
describe('Listbox.Option', () => {
|
|
it(
|
|
'should be possible to render a Listbox.Option using a render prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">{(slot) => <>{JSON.stringify(slot)}</>}</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await click(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({
|
|
state: ListboxState.Visible,
|
|
textContent: JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
}),
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should guarantee the order of DOM nodes when performing actions', async () => {
|
|
function Example({ hide = false }) {
|
|
return (
|
|
<>
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option 1</Listbox.Option>
|
|
{!hide && <Listbox.Option value="b">Option 2</Listbox.Option>}
|
|
<Listbox.Option value="c">Option 3</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
</>
|
|
)
|
|
}
|
|
|
|
let { rerender } = render(<Example />)
|
|
|
|
// Open the Listbox
|
|
await click(getByText('Trigger'))
|
|
|
|
rerender(<Example hide={true} />) // Remove Listbox.Option 2
|
|
rerender(<Example hide={false} />) // Re-add Listbox.Option 2
|
|
|
|
assertListbox({ state: ListboxState.Visible })
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Focus the first item
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify that the first menu item is active
|
|
assertActiveListboxOption(options[0])
|
|
|
|
await press(Keys.ArrowDown)
|
|
// Verify that the second menu item is active
|
|
assertActiveListboxOption(options[1])
|
|
|
|
await press(Keys.ArrowDown)
|
|
// Verify that the third menu item is active
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
|
|
describe('Uncontrolled', () => {
|
|
it('should be possible to use in an uncontrolled way', async () => {
|
|
let handleSubmission = jest.fn()
|
|
|
|
render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">Alice</Listbox.Option>
|
|
<Listbox.Option value="bob">Bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">Charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// No values
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({})
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Submit
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Alice should be submitted
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'alice' })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose charlie
|
|
await click(getListboxOptions()[2])
|
|
|
|
// Submit
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Charlie should be submitted
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'charlie' })
|
|
})
|
|
|
|
it('should expose the value via the render prop', async () => {
|
|
let handleSubmission = jest.fn()
|
|
|
|
let { getByTestId } = render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee">
|
|
{({ value }) => (
|
|
<>
|
|
<div data-testid="value">{value}</div>
|
|
<Listbox.Button>
|
|
{({ value }) => (
|
|
<>
|
|
Trigger
|
|
<div data-testid="value-2">{value}</div>
|
|
</>
|
|
)}
|
|
</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">Alice</Listbox.Option>
|
|
<Listbox.Option value="bob">Bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">Charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</>
|
|
)}
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// No values
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({})
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
expect(getByTestId('value')).toHaveTextContent('alice')
|
|
expect(getByTestId('value-2')).toHaveTextContent('alice')
|
|
|
|
// Submit
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Alice should be submitted
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'alice' })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose charlie
|
|
await click(getListboxOptions()[2])
|
|
expect(getByTestId('value')).toHaveTextContent('charlie')
|
|
expect(getByTestId('value-2')).toHaveTextContent('charlie')
|
|
|
|
// Submit
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Charlie should be submitted
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'charlie' })
|
|
})
|
|
|
|
it('should be possible to provide a default value', async () => {
|
|
let handleSubmission = jest.fn()
|
|
|
|
render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee" defaultValue="bob">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">Alice</Listbox.Option>
|
|
<Listbox.Option value="bob">Bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">Charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Bob is the defaultValue
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'bob' })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Submit
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Alice should be submitted
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'alice' })
|
|
})
|
|
|
|
it('should be possible to reset to the default value if the form is reset', async () => {
|
|
let handleSubmission = jest.fn()
|
|
|
|
render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee" defaultValue="bob">
|
|
<Listbox.Button>{({ value }) => value ?? 'Trigger'}</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">Alice</Listbox.Option>
|
|
<Listbox.Option value="bob">Bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">Charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
<button type="reset" id="reset">
|
|
reset
|
|
</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Bob is the defaultValue
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'bob' })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Reset
|
|
await click(document.getElementById('reset'))
|
|
|
|
// The listbox should be reset to bob
|
|
expect(getListboxButton()).toHaveTextContent('bob')
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertActiveListboxOption(getListboxOptions()[1])
|
|
})
|
|
|
|
it('should be possible to reset to the default value if the form is reset (using objects)', async () => {
|
|
let handleSubmission = jest.fn()
|
|
|
|
let data = [
|
|
{ id: 1, name: 'alice', label: 'Alice' },
|
|
{ id: 2, name: 'bob', label: 'Bob' },
|
|
{ id: 3, name: 'charlie', label: 'Charlie' },
|
|
]
|
|
|
|
render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee" defaultValue={{ id: 2, name: 'bob', label: 'Bob' }} by="id">
|
|
<Listbox.Button>{({ value }) => value?.name ?? 'Trigger'}</Listbox.Button>
|
|
<Listbox.Options>
|
|
{data.map((person) => (
|
|
<Listbox.Option key={person.id} value={person}>
|
|
{person.label}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
<button type="reset" id="reset">
|
|
reset
|
|
</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Bob is the defaultValue
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({
|
|
'assignee[id]': '2',
|
|
'assignee[name]': 'bob',
|
|
'assignee[label]': 'Bob',
|
|
})
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Reset
|
|
await click(document.getElementById('reset'))
|
|
|
|
// The listbox should be reset to bob
|
|
expect(getListboxButton()).toHaveTextContent('bob')
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertActiveListboxOption(getListboxOptions()[1])
|
|
})
|
|
|
|
it('should be possible to reset to the default value in multiple mode', async () => {
|
|
let handleSubmission = jest.fn()
|
|
let data = ['alice', 'bob', 'charlie']
|
|
|
|
render(
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
|
}}
|
|
>
|
|
<Listbox name="assignee" defaultValue={['bob'] as string[]} multiple>
|
|
<Listbox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Listbox.Button>
|
|
<Listbox.Options>
|
|
{data.map((person) => (
|
|
<Listbox.Option key={person} value={person}>
|
|
{person}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button id="submit">submit</button>
|
|
<button type="reset" id="reset">
|
|
reset
|
|
</button>
|
|
</form>
|
|
)
|
|
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Bob is the defaultValue
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({
|
|
'assignee[0]': 'bob',
|
|
})
|
|
|
|
await click(document.getElementById('reset'))
|
|
await click(document.getElementById('submit'))
|
|
|
|
// Bob is still the defaultValue
|
|
expect(handleSubmission).toHaveBeenLastCalledWith({
|
|
'assignee[0]': 'bob',
|
|
})
|
|
})
|
|
|
|
it('should still call the onChange listeners when choosing new values', async () => {
|
|
let handleChange = jest.fn()
|
|
|
|
render(
|
|
<Listbox name="assignee" onChange={handleChange}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">Alice</Listbox.Option>
|
|
<Listbox.Option value="bob">Bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">Charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose alice
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose bob
|
|
await click(getListboxOptions()[1])
|
|
|
|
// Change handler should have been called twice
|
|
expect(handleChange).toHaveBeenNthCalledWith(1, 'alice')
|
|
expect(handleChange).toHaveBeenNthCalledWith(2, 'bob')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Rendering composition', () => {
|
|
it(
|
|
'should be possible to conditionally render classNames (aka className can be a function?!)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a" className={(bag) => JSON.stringify(bag)}>
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option value="b" disabled className={(bag) => JSON.stringify(bag)}>
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c" className="no-special-treatment">
|
|
Option C
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open Listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Verify correct classNames
|
|
expect('' + options[0].classList).toEqual(
|
|
JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[1].classList).toEqual(
|
|
JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: true,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[2].classList).toEqual('no-special-treatment')
|
|
|
|
// Double check that nothing is active
|
|
assertNoActiveListboxOption(getListbox())
|
|
|
|
// Make the first option active
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify the classNames
|
|
expect('' + options[0].classList).toEqual(
|
|
JSON.stringify({
|
|
active: true,
|
|
focus: true,
|
|
selected: false,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[1].classList).toEqual(
|
|
JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: true,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[2].classList).toEqual('no-special-treatment')
|
|
|
|
// Double check that the first option is the active one
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// Let's go down, this should go to the third option since the second option is disabled!
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify the classNames
|
|
expect('' + options[0].classList).toEqual(
|
|
JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[1].classList).toEqual(
|
|
JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: true,
|
|
selectedOption: false,
|
|
})
|
|
)
|
|
expect('' + options[2].classList).toEqual('no-special-treatment')
|
|
|
|
// Double check that the last option is the active one
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to swap the Listbox option with a button for example',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option as="button" value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option as="button" value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option as="button" value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open Listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify options are buttons now
|
|
getListboxOptions().forEach((option) => assertListboxOption(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 Listbox.Options with a Transition component',
|
|
suppressConsoleLogs(async () => {
|
|
let orderFn = jest.fn()
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Debug name="Listbox" fn={orderFn} />
|
|
<Transition>
|
|
<Debug name="Transition" fn={orderFn} />
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">
|
|
{(data) => (
|
|
<>
|
|
{JSON.stringify(data)}
|
|
<Debug name="Listbox.Option" fn={orderFn} />
|
|
</>
|
|
)}
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Transition>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
await rawClick(getListboxButton())
|
|
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({
|
|
state: ListboxState.Visible,
|
|
textContent: JSON.stringify({
|
|
active: false,
|
|
focus: false,
|
|
selected: false,
|
|
disabled: false,
|
|
selectedOption: false,
|
|
}),
|
|
})
|
|
|
|
await rawClick(getListboxButton())
|
|
|
|
// Verify that we tracked the `mounts` and `unmounts` in the correct order
|
|
expect(orderFn.mock.calls).toEqual([
|
|
['Mounting - Listbox'],
|
|
['Mounting - Transition'],
|
|
['Mounting - Listbox.Option'],
|
|
['Unmounting - Transition'],
|
|
['Unmounting - Listbox.Option'],
|
|
])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Keyboard interactions', () => {
|
|
describe('`Enter` key', () => {
|
|
it(
|
|
'should be possible to close the listbox with Enter when there is no active listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
|
|
// Close listbox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify it is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to close the listbox with Enter and choose the active listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Listbox
|
|
value={value}
|
|
onChange={(value) => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
|
|
// Activate the first listbox option
|
|
let options = getListboxOptions()
|
|
await mouseMove(options[0])
|
|
|
|
// Choose option, and close listbox
|
|
await press(Keys.Enter)
|
|
|
|
// Verify it is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify we got the change event
|
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
|
expect(handleChange).toHaveBeenCalledWith('a')
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Verify the active option is the previously selected one
|
|
assertActiveListboxOption(getListboxOptions()[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Space` key', () => {
|
|
it(
|
|
'should be possible to open the listbox with Space',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the listbox with Space when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Try to open the listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is still closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the listbox with Space, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value="b" onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second listbox option is active (because it is already selected)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option when there are no listbox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options />
|
|
</Listbox>
|
|
)
|
|
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should focus the first non disabled listbox option when opening with Space',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Verify that the first non-disabled listbox option is active
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should focus the first non disabled listbox option when opening with Space (jump over multiple disabled ones)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Verify that the first non-disabled listbox option is active
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option upon Space key press, when there are no non-disabled listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to close the listbox with Space and choose the active listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Listbox
|
|
value={value}
|
|
onChange={(value) => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
|
|
// Activate the first listbox option
|
|
let options = getListboxOptions()
|
|
await mouseMove(options[0])
|
|
|
|
// Choose option, and close listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify we got the change event
|
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
|
expect(handleChange).toHaveBeenCalledWith('a')
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Verify the active option is the previously selected one
|
|
assertActiveListboxOption(getListboxOptions()[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Escape` key', () => {
|
|
it(
|
|
'should be possible to close an open listbox with Escape',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Close listbox
|
|
await press(Keys.Escape)
|
|
|
|
// Verify it is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Tab` key', () => {
|
|
it(
|
|
'should not focus trap when we use Tab',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<>
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<a href="#">After</a>
|
|
</>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// Tab to the next element
|
|
await press(Keys.Tab)
|
|
|
|
// Verify the listbox is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
assertActiveElement(getByText('After'))
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not focus trap when we use Shift+Tab',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<>
|
|
<a href="#">Before</a>
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
</>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// Try to Shift+Tab
|
|
await press(shift(Keys.Tab))
|
|
|
|
// Verify the listbox is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
assertActiveElement(getByText('Before'))
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowDown` key', () => {
|
|
it(
|
|
'should be possible to open the listbox with ArrowDown',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
|
|
// Verify that the first listbox option is active
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the listbox with ArrowDown when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Try to open the listbox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is still closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the listbox with ArrowDown, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value="b" onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowDown)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second listbox option is active (because it is already selected)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option when there are no listbox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options />
|
|
</Listbox>
|
|
)
|
|
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowDown)
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowDown)
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowDown)
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should NOT be able to go down again (because last option). Current implementation won't go around.
|
|
await press(Keys.ArrowDown)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the listbox options and skip the first disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowDown)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowDown to navigate the listbox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowRight` key', () => {
|
|
it(
|
|
'should be possible to use ArrowRight to navigate the listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} horizontal>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go right once
|
|
await press(Keys.ArrowRight)
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go right again
|
|
await press(Keys.ArrowRight)
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should NOT be able to go right again (because last option). Current implementation won't go around.
|
|
await press(Keys.ArrowRight)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowUp` key', () => {
|
|
it(
|
|
'should be possible to open the listbox with ArrowUp and the last option should be active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
|
|
// ! ALERT: The LAST option should now be active
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the listbox with ArrowUp and the last option should be active when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Try to open the listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is still closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the listbox with ArrowUp, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value="b" onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second listbox option is active (because it is already selected)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option when there are no listbox options at all',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options />
|
|
</Listbox>
|
|
)
|
|
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowUp to navigate the listbox options and jump to the first non-disabled one',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to navigate up or down if there is only a single non-disabled option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should not be able to go up (because those are disabled)
|
|
await press(Keys.ArrowUp)
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should not be able to go down (because this is the last option)
|
|
await press(Keys.ArrowDown)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use ArrowUp to navigate the listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go down once
|
|
await press(Keys.ArrowUp)
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go down again
|
|
await press(Keys.ArrowUp)
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should NOT be able to go up again (because first option). Current implementation won't go around.
|
|
await press(Keys.ArrowUp)
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`ArrowLeft` key', () => {
|
|
it(
|
|
'should be possible to use ArrowLeft to navigate the listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} horizontal>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible, orientation: 'horizontal' })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go left once
|
|
await press(Keys.ArrowLeft)
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go left again
|
|
await press(Keys.ArrowLeft)
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should NOT be able to go left again (because first option). Current implementation won't go around.
|
|
await press(Keys.ArrowLeft)
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`End` key', () => {
|
|
it(
|
|
'should be possible to use the End key to go to the last listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first option
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await press(Keys.End)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the End key to go to the last non disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first option
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last non-disabled option
|
|
await press(Keys.End)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the End key to go to the first listbox option if that is the only non-disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.End)
|
|
|
|
let options = getListboxOptions()
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option upon End key press, when there are no non-disabled listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.End)
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`PageDown` key', () => {
|
|
it(
|
|
'should be possible to use the PageDown key to go to the last listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first option
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await press(Keys.PageDown)
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageDown key to go to the last non disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.Space)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first option
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last non-disabled option
|
|
await press(Keys.PageDown)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageDown key to go to the first listbox option if that is the only non-disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageDown)
|
|
|
|
let options = getListboxOptions()
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option upon PageDown key press, when there are no non-disabled listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageDown)
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Home` key', () => {
|
|
it(
|
|
'should be possible to use the Home key to go to the first listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go to the first option
|
|
await press(Keys.Home)
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the Home key to go to the first non disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
<Listbox.Option value="d">Option D</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first non-disabled option
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the Home key to go to the last listbox option if that is the only non-disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option value="d">Option D</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
let options = getListboxOptions()
|
|
assertActiveListboxOption(options[3])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option upon Home key press, when there are no non-disabled listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.Home)
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`PageUp` key', () => {
|
|
it(
|
|
'should be possible to use the PageUp key to go to the first listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go to the first option
|
|
await press(Keys.PageUp)
|
|
assertActiveListboxOption(options[0])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageUp key to go to the first non disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
<Listbox.Option value="d">Option D</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the first non-disabled option
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to use the PageUp key to go to the last listbox option if that is the only non-disabled listbox option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option value="d">Option D</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
let options = getListboxOptions()
|
|
assertActiveListboxOption(options[3])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should have no active listbox option upon PageUp key press, when there are no non-disabled listbox options',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option disabled value="a">
|
|
Option A
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="b">
|
|
Option B
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="c">
|
|
Option C
|
|
</Listbox.Option>
|
|
<Listbox.Option disabled value="d">
|
|
Option D
|
|
</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// We opened via click, we don't have an active option
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should not be able to go to the end
|
|
await press(Keys.PageUp)
|
|
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('`Any` key aka search', () => {
|
|
it(
|
|
'should be possible to type a full word that has a perfect match',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('bob'))
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('alice'))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('charlie'))
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to type a partial of a word',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('bo'))
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('ali'))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('char'))
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to type words with spaces',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">value a</Listbox.Option>
|
|
<Listbox.Option value="b">value b</Listbox.Option>
|
|
<Listbox.Option value="c">value c</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should be able to go to the second option
|
|
await type(word('value b'))
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go to the first option
|
|
await type(word('value a'))
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await type(word('value c'))
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to search for a disabled option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// We should not be able to go to the disabled option
|
|
await type(word('bo'))
|
|
|
|
// We should still be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to search for a word (case insensitive)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Open listbox
|
|
await press(Keys.ArrowUp)
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be on the last option
|
|
assertActiveListboxOption(options[2])
|
|
|
|
// Search for bob in a different casing
|
|
await type(word('BO'))
|
|
|
|
// We should be on `bob`
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to search for the next occurrence',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">alice</Listbox.Option>
|
|
<Listbox.Option value="b">bob</Listbox.Option>
|
|
<Listbox.Option value="c">charlie</Listbox.Option>
|
|
<Listbox.Option value="d">bob</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Search for bob
|
|
await type(word('b'))
|
|
|
|
// We should be on the first `bob`
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// Search for bob again
|
|
await type(word('b'))
|
|
|
|
// We should be on the second `bob`
|
|
assertActiveListboxOption(options[3])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should stay on the same item while keystrokes still match',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">alice</Listbox.Option>
|
|
<Listbox.Option value="b">bob</Listbox.Option>
|
|
<Listbox.Option value="c">charlie</Listbox.Option>
|
|
<Listbox.Option value="d">bob</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// ---
|
|
|
|
// Reset: Go to first option
|
|
await press(Keys.Home)
|
|
|
|
// Search for "b" in "bob"
|
|
await type(word('b'))
|
|
|
|
// We should be on the first `bob`
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// Search for "b" in "bob" again
|
|
await type(word('b'))
|
|
|
|
// We should be on the next `bob`
|
|
assertActiveListboxOption(options[3])
|
|
|
|
// ---
|
|
|
|
// Reset: Go to first option
|
|
await press(Keys.Home)
|
|
|
|
// Search for "bo" in "bob"
|
|
await type(word('bo'))
|
|
|
|
// We should be on the first `bob`
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// Search for "bo" in "bob" again
|
|
await type(word('bo'))
|
|
|
|
// We should be on the next `bob`
|
|
assertActiveListboxOption(options[3])
|
|
|
|
// ---
|
|
|
|
// Reset: Go to first option
|
|
await press(Keys.Home)
|
|
|
|
// Search for "bob" in "bob"
|
|
await type(word('bob'))
|
|
|
|
// We should be on the first `bob`
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// Search for "bob" in "bob" again
|
|
await type(word('bob'))
|
|
|
|
// We should be on the next `bob`
|
|
assertActiveListboxOption(options[3])
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Mouse interactions', () => {
|
|
it(
|
|
'should focus the Listbox.Button when we click the Listbox.Label',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Label>Label</Listbox.Label>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Ensure the button is not focused yet
|
|
assertActiveElement(document.body)
|
|
|
|
// Focus the label
|
|
await click(getListboxLabel())
|
|
|
|
// Ensure that the actual button is focused instead
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not focus the Listbox.Button when we right click the Listbox.Label',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Label>Label</Listbox.Label>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Ensure the button is not focused yet
|
|
assertActiveElement(document.body)
|
|
|
|
// Focus the label
|
|
await click(getListboxLabel(), MouseButton.Right)
|
|
|
|
// Ensure that the body is still active
|
|
assertActiveElement(document.body)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the listbox on click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option) => assertListboxOption(option))
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the listbox on right click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Item A</Listbox.Option>
|
|
<Listbox.Option value="b">Item B</Listbox.Option>
|
|
<Listbox.Option value="c">Item C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Try to open the listbox
|
|
await click(getListboxButton(), MouseButton.Right)
|
|
|
|
// Verify it is still closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to open the listbox on click when the button is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)} disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Try to open the listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is still closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to open the listbox on click, and focus the selected option',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value="b" onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
assertListboxButtonLinkedWithListbox()
|
|
|
|
// Verify we have listbox options
|
|
let options = getListboxOptions()
|
|
expect(options).toHaveLength(3)
|
|
options.forEach((option, i) => assertListboxOption(option, { selected: i === 1 }))
|
|
|
|
// Verify that the second listbox option is active (because it is already selected)
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to close a listbox on click',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="a">Option A</Listbox.Option>
|
|
<Listbox.Option value="b">Option B</Listbox.Option>
|
|
<Listbox.Option value="c">Option C</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is visible
|
|
assertListboxButton({ state: ListboxState.Visible })
|
|
|
|
// Click to close
|
|
await click(getListboxButton())
|
|
|
|
// Verify it is closed
|
|
assertListboxButton({ state: ListboxState.InvisibleUnmounted })
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we click outside of a closed listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Verify that the window is closed
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Click something that is not related to the listbox
|
|
await click(document.body)
|
|
|
|
// Should still be closed
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the listbox which should close the listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
// Click something that is not related to the listbox
|
|
await click(document.body)
|
|
|
|
// Should be closed now
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the listbox on another listbox button which should close the current listbox and open the new listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<div>
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
</div>
|
|
)
|
|
|
|
let [button1, button2] = getListboxButtons()
|
|
|
|
// Click the first listbox button
|
|
await click(button1)
|
|
expect(getListboxes()).toHaveLength(1) // Only 1 listbox should be visible
|
|
|
|
// Ensure the open listbox is linked to the first button
|
|
assertListboxButtonLinkedWithListbox(button1, getListbox())
|
|
|
|
// Click the second listbox button
|
|
await click(button2)
|
|
|
|
expect(getListboxes()).toHaveLength(1) // Only 1 listbox should be visible
|
|
|
|
// Ensure the open listbox is linked to the second button
|
|
assertListboxButtonLinkedWithListbox(button2, getListbox())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the listbox which should close the listbox (even if we press the listbox button)',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
// Click the listbox button again
|
|
await click(getListboxButton())
|
|
|
|
// Should be closed now
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click outside of the listbox, on an element which is within a focusable element, which closes the listbox',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<div>
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
|
|
<button id="btn">
|
|
<span>Next</span>
|
|
</button>
|
|
</div>
|
|
)
|
|
|
|
// Click the listbox button
|
|
await click(getListboxButton())
|
|
|
|
// Ensure the listbox is open
|
|
assertListbox({ state: ListboxState.Visible })
|
|
|
|
// Click the span inside the button
|
|
await click(getByText('Next'))
|
|
|
|
// Ensure the listbox is closed
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
|
|
// Ensure the outside button is focused
|
|
assertActiveElement(document.getElementById('btn'))
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to hover an option and make it active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveListboxOption(options[1])
|
|
|
|
// We should be able to go to the first option
|
|
await mouseMove(options[0])
|
|
assertActiveListboxOption(options[0])
|
|
|
|
// We should be able to go to the last option
|
|
await mouseMove(options[2])
|
|
assertActiveListboxOption(options[2])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should make a listbox option active when you move the mouse over it',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we move the mouse and the listbox option is already active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveListboxOption(options[1])
|
|
|
|
await mouseMove(options[1])
|
|
|
|
// Nothing should be changed
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be a no-op when we move the mouse and the listbox option is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
await mouseMove(options[1])
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to hover an option that is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Try to hover over option 1, which is disabled
|
|
await mouseMove(options[1])
|
|
|
|
// We should not have an active option now
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to mouse leave an option and make it inactive',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value="bob" onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be able to go to the second option
|
|
await mouseMove(options[1])
|
|
assertActiveListboxOption(options[1])
|
|
|
|
await mouseLeave(options[1])
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should be able to go to the first option
|
|
await mouseMove(options[0])
|
|
assertActiveListboxOption(options[0])
|
|
|
|
await mouseLeave(options[0])
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should be able to go to the last option
|
|
await mouseMove(options[2])
|
|
assertActiveListboxOption(options[2])
|
|
|
|
await mouseLeave(options[2])
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to mouse leave a disabled option and be a no-op',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Try to hover over option 1, which is disabled
|
|
await mouseMove(options[1])
|
|
assertNoActiveListboxOption()
|
|
|
|
await mouseLeave(options[1])
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click a listbox option, which closes the listbox',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Listbox
|
|
value={value}
|
|
onChange={(value) => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be able to click the first option
|
|
await click(options[1])
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
|
expect(handleChange).toHaveBeenCalledWith('bob')
|
|
|
|
// Verify the button is focused again
|
|
assertActiveElement(getListboxButton())
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Verify the active option is the previously selected one
|
|
assertActiveListboxOption(getListboxOptions()[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible to click a disabled listbox option, which is a no-op',
|
|
suppressConsoleLogs(async () => {
|
|
let handleChange = jest.fn()
|
|
function Example() {
|
|
let [value, setValue] = useState(undefined)
|
|
|
|
return (
|
|
<Listbox
|
|
value={value}
|
|
onChange={(value) => {
|
|
setValue(value)
|
|
handleChange(value)
|
|
}}
|
|
>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should be able to click the first option
|
|
await click(options[1])
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
expect(handleChange).toHaveBeenCalledTimes(0)
|
|
|
|
// Close the listbox
|
|
await click(getListboxButton())
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Verify the active option is non existing
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should be possible focus a listbox option, so that it becomes active',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// Verify that nothing is active yet
|
|
assertNoActiveListboxOption()
|
|
|
|
// We should be able to focus the first option
|
|
await focus(options[1])
|
|
assertActiveListboxOption(options[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should not be possible to focus a listbox option which is disabled',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox value={undefined} onChange={(x) => console.log(x)}>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option disabled value="bob">
|
|
bob
|
|
</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
assertActiveElement(getListbox())
|
|
|
|
let options = getListboxOptions()
|
|
|
|
// We should not be able to focus the first option
|
|
await focus(options[1])
|
|
assertNoActiveListboxOption()
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Multi-select', () => {
|
|
it(
|
|
'should be possible to pass multiple values to the Listbox component',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} multiple>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify that we have an open listbox with multiple mode
|
|
assertListbox({ state: ListboxState.Visible, mode: ListboxMode.Multiple })
|
|
|
|
// Verify that we have multiple selected listbox options
|
|
let options = getListboxOptions()
|
|
|
|
assertListboxOption(options[0], { selected: false })
|
|
assertListboxOption(options[1], { selected: true })
|
|
assertListboxOption(options[2], { selected: true })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should make the first selected option the active item',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} multiple>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Verify that bob is the active option
|
|
assertActiveListboxOption(getListboxOptions()[1])
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should keep the listbox open when selecting an item via the keyboard',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} multiple>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
|
|
// Verify that bob is the active option
|
|
await click(getListboxOptions()[0])
|
|
|
|
// Verify that the listbox is still open
|
|
assertListbox({ state: ListboxState.Visible })
|
|
})
|
|
)
|
|
|
|
it(
|
|
'should toggle the selected state of an option when clicking on it',
|
|
suppressConsoleLogs(async () => {
|
|
function Example() {
|
|
let [value, setValue] = useState<string[]>(['bob', 'charlie'])
|
|
|
|
return (
|
|
<Listbox value={value} onChange={setValue} multiple>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="alice">alice</Listbox.Option>
|
|
<Listbox.Option value="bob">bob</Listbox.Option>
|
|
<Listbox.Option value="charlie">charlie</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
assertListbox({ state: ListboxState.Visible })
|
|
|
|
let options = getListboxOptions()
|
|
|
|
assertListboxOption(options[0], { selected: false })
|
|
assertListboxOption(options[1], { selected: true })
|
|
assertListboxOption(options[2], { selected: true })
|
|
|
|
// Click on bob
|
|
await click(getListboxOptions()[1])
|
|
|
|
assertListboxOption(options[0], { selected: false })
|
|
assertListboxOption(options[1], { selected: false })
|
|
assertListboxOption(options[2], { selected: true })
|
|
|
|
// Click on bob again
|
|
await click(getListboxOptions()[1])
|
|
|
|
assertListboxOption(options[0], { selected: false })
|
|
assertListboxOption(options[1], { selected: true })
|
|
assertListboxOption(options[2], { selected: true })
|
|
})
|
|
)
|
|
})
|
|
|
|
describe('Form compatibility', () => {
|
|
it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
|
|
let submits = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(null)
|
|
return (
|
|
<div>
|
|
<Listbox form="my-form" value={value} onChange={setValue} name="delivery">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Label>Pizza Delivery</Listbox.Label>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="pickup">Pickup</Listbox.Option>
|
|
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
|
|
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
|
|
<form
|
|
id="my-form"
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submits([...new FormData(event.currentTarget).entries()])
|
|
}}
|
|
>
|
|
<button>Submit</button>
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose pickup
|
|
await click(getByText('Pickup'))
|
|
|
|
// Submit the form
|
|
await click(getByText('Submit'))
|
|
|
|
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
|
|
})
|
|
|
|
it('should be possible to submit a form by pressing enter', async () => {
|
|
let submits = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(null)
|
|
return (
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submits([...new FormData(event.currentTarget).entries()])
|
|
}}
|
|
>
|
|
<Listbox value={value} onChange={setValue} name="delivery">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Label>Pizza Delivery</Listbox.Label>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="pickup">Pickup</Listbox.Option>
|
|
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
|
|
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button>Submit</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Focus the listbox
|
|
await focus(getListboxButton())
|
|
|
|
// Submit the form by pressing enter
|
|
await press(Keys.Enter)
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([]) // no data
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Choose home delivery
|
|
await click(getByText('Home delivery'))
|
|
|
|
// Focus the listbox
|
|
await focus(getListboxButton())
|
|
|
|
// Submit the form by pressing enter
|
|
await press(Keys.Enter)
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([['delivery', 'home-delivery']])
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Choose pickup
|
|
await click(getByText('Pickup'))
|
|
|
|
// Focus the listbox
|
|
await focus(getListboxButton())
|
|
|
|
// Submit the form by pressing enter
|
|
await press(Keys.Enter)
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
|
|
})
|
|
|
|
it('should be possible to submit a form with a value', async () => {
|
|
let submits = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(null)
|
|
return (
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submits([...new FormData(event.currentTarget).entries()])
|
|
}}
|
|
>
|
|
<Listbox value={value} onChange={setValue} name="delivery">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Label>Pizza Delivery</Listbox.Label>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="pickup">Pickup</Listbox.Option>
|
|
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
|
|
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button>Submit</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Submit the form
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([]) // no data
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Choose home delivery
|
|
await click(getByText('Home delivery'))
|
|
|
|
// Submit the form again
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([['delivery', 'home-delivery']])
|
|
|
|
// Open listbox again
|
|
await click(getListboxButton())
|
|
|
|
// Choose pickup
|
|
await click(getByText('Pickup'))
|
|
|
|
// Submit the form again
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([['delivery', 'pickup']])
|
|
})
|
|
|
|
it('should not submit the data if the Listbox is disabled', async () => {
|
|
let submits = jest.fn()
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState('home-delivery')
|
|
return (
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submits([...new FormData(event.currentTarget).entries()])
|
|
}}
|
|
>
|
|
<input type="hidden" name="foo" value="bar" />
|
|
<Listbox value={value} onChange={setValue} name="delivery" disabled>
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Label>Pizza Delivery</Listbox.Label>
|
|
<Listbox.Options>
|
|
<Listbox.Option value="pickup">Pickup</Listbox.Option>
|
|
<Listbox.Option value="home-delivery">Home delivery</Listbox.Option>
|
|
<Listbox.Option value="dine-in">Dine in</Listbox.Option>
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button>Submit</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Submit the form
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([
|
|
['foo', 'bar'], // The only available field
|
|
])
|
|
})
|
|
|
|
it('should be possible to submit a form with a complex value object', async () => {
|
|
let submits = jest.fn()
|
|
let options = [
|
|
{
|
|
id: 1,
|
|
value: 'pickup',
|
|
label: 'Pickup',
|
|
extra: { info: 'Some extra info' },
|
|
},
|
|
{
|
|
id: 2,
|
|
value: 'home-delivery',
|
|
label: 'Home delivery',
|
|
extra: { info: 'Some extra info' },
|
|
},
|
|
{
|
|
id: 3,
|
|
value: 'dine-in',
|
|
label: 'Dine in',
|
|
extra: { info: 'Some extra info' },
|
|
},
|
|
]
|
|
|
|
function Example() {
|
|
let [value, setValue] = useState(options[0])
|
|
|
|
return (
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submits([...new FormData(event.currentTarget).entries()])
|
|
}}
|
|
>
|
|
<Listbox value={value} onChange={setValue} name="delivery">
|
|
<Listbox.Button>Trigger</Listbox.Button>
|
|
<Listbox.Label>Pizza Delivery</Listbox.Label>
|
|
<Listbox.Options>
|
|
{options.map((option) => (
|
|
<Listbox.Option key={option.id} value={option}>
|
|
{option.label}
|
|
</Listbox.Option>
|
|
))}
|
|
</Listbox.Options>
|
|
</Listbox>
|
|
<button>Submit</button>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
render(<Example />)
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Submit the form
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([
|
|
['delivery[id]', '1'],
|
|
['delivery[value]', 'pickup'],
|
|
['delivery[label]', 'Pickup'],
|
|
['delivery[extra][info]', 'Some extra info'],
|
|
])
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose home delivery
|
|
await click(getByText('Home delivery'))
|
|
|
|
// Submit the form again
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([
|
|
['delivery[id]', '2'],
|
|
['delivery[value]', 'home-delivery'],
|
|
['delivery[label]', 'Home delivery'],
|
|
['delivery[extra][info]', 'Some extra info'],
|
|
])
|
|
|
|
// Open listbox
|
|
await click(getListboxButton())
|
|
|
|
// Choose pickup
|
|
await click(getByText('Pickup'))
|
|
|
|
// Submit the form again
|
|
await click(getByText('Submit'))
|
|
|
|
// Verify that the form has been submitted
|
|
expect(submits).toHaveBeenLastCalledWith([
|
|
['delivery[id]', '1'],
|
|
['delivery[value]', 'pickup'],
|
|
['delivery[label]', 'Pickup'],
|
|
['delivery[extra][info]', 'Some extra info'],
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('transitions', () => {
|
|
it(
|
|
'should be possible to close the Listbox when using the `transition` prop',
|
|
suppressConsoleLogs(async () => {
|
|
render(
|
|
<Listbox>
|
|
<ListboxButton>Toggle</ListboxButton>
|
|
<ListboxOptions transition>
|
|
<ListboxOption value="alice">Alice</ListboxOption>
|
|
<ListboxOption value="bob">Bob</ListboxOption>
|
|
<ListboxOption value="charlie">Charlie</ListboxOption>
|
|
</ListboxOptions>
|
|
</Listbox>
|
|
)
|
|
|
|
// Focus the button
|
|
await focus(getListboxButton())
|
|
|
|
// Ensure the button is focused
|
|
assertActiveElement(getListboxButton())
|
|
|
|
// Open the listbox
|
|
await click(getListboxButton())
|
|
|
|
// Ensure the listbox is visible
|
|
assertListbox({ state: ListboxState.Visible })
|
|
|
|
// Close the listbox
|
|
await click(getListboxButton())
|
|
|
|
// Wait for the transition to finish, and the listbox to close
|
|
await waitFor(() => {
|
|
assertListbox({ state: ListboxState.InvisibleUnmounted })
|
|
})
|
|
|
|
// Ensure the button got the restored focus
|
|
assertActiveElement(getListboxButton())
|
|
})
|
|
)
|
|
})
|