Skip to content

Commit

Permalink
Add multi value support for Listbox & Combobox (#1243)
Browse files Browse the repository at this point in the history
* First attempt at a multi-listbox

* implement `multiple` mode on Listbox

* add multiple Listbox example to playground

* implement `multiple` mode on Combobox

* make sure groupContext is not undefined or null

On vercel, getting a strange issue like `TypeError: undefined is not an
object (evaluating 'r.resolveTarget')` which doesn't happen locally or
once published. Would expect it to be `null` since we default to `null`.
Hopefully this fixes things.

* bump all the dependencies

* make sure that `@types/react` use set to the correct version

`@types/react-dom` hardcoded the `@types/react` to version `16.14.21`
instead of using the latest `16.14.24` resulting in type mismatches.

*cries in inconsistency*

* update changelog

* add multiple Combobox example to playground

* refactor Combobox, use actions

* use combobox data

This is a first step in refactoring everything where we use dedicated
actions and data instead of accessing the reducer state directly.

It also allows us to get rid of mutations in render where we updated
some values in render directly which is not ideal.

Co-authored-by: pvanliefland <pierre.vanliefland@gmail.com>
  • Loading branch information
RobinMalfait and pvanliefland authored Mar 16, 2022
1 parent 63383c4 commit 40fee45
Show file tree
Hide file tree
Showing 19 changed files with 2,310 additions and 976 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve some internal code ([#1221](https://github.com/tailwindlabs/headlessui/pull/1221))
- Use `ownerDocument` instead of `document` ([#1158](https://github.com/tailwindlabs/headlessui/pull/1158))
- Ensure focus trap, Tabs and Dialog play well together ([#1231](https://github.com/tailwindlabs/headlessui/pull/1231))
- Add `multi` value support for Listbox & Combobox ([#1243](https://github.com/tailwindlabs/headlessui/pull/1243))

### Added

Expand Down Expand Up @@ -54,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `ownerDocument` instead of `document` ([#1158](https://github.com/tailwindlabs/headlessui/pull/1158))
- Re-expose `el` ([#1230](https://github.com/tailwindlabs/headlessui/pull/1230))
- Ensure focus trap, Tabs and Dialog play well together ([#1231](https://github.com/tailwindlabs/headlessui/pull/1231))
- Add `multi` value support for Listbox & Combobox ([#1243](https://github.com/tailwindlabs/headlessui/pull/1243))

### Added

Expand Down
8 changes: 4 additions & 4 deletions packages/@headlessui-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
},
"devDependencies": {
"@testing-library/react": "^11.2.3",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/react": "16.14.21",
"@types/react-dom": "^16.9.0",
"esbuild": "^0.11.18",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"snapshot-diff": "^0.8.1",
"esbuild": "^0.11.18"
"snapshot-diff": "^0.8.1"
}
}
149 changes: 149 additions & 0 deletions packages/@headlessui-react/src/components/combobox/combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
ComboboxState,
getByText,
getComboboxes,
assertCombobox,
ComboboxMode,
} from '../../test-utils/accessibility-assertions'
import { Transition } from '../transitions/transition'

Expand Down Expand Up @@ -4393,6 +4395,153 @@ describe('Mouse interactions', () => {
)
})

describe('Multi-select', () => {
it(
'should be possible to pass multiple values to the Combobox component',
suppressConsoleLogs(async () => {
function Example() {
let [value, setValue] = useState<string[]>(['bob', 'charlie'])

return (
<Combobox value={value} onChange={setValue}>
<Combobox.Input onChange={() => {}} />
<Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options>
<Combobox.Option value="alice">alice</Combobox.Option>
<Combobox.Option value="bob">bob</Combobox.Option>
<Combobox.Option value="charlie">charlie</Combobox.Option>
</Combobox.Options>
</Combobox>
)
}

render(<Example />)

// Open combobox
await click(getComboboxButton())

// Verify that we have an open combobox with multiple mode
assertCombobox({ state: ComboboxState.Visible, mode: ComboboxMode.Multiple })

// Verify that we have multiple selected combobox options
let options = getComboboxOptions()

assertComboboxOption(options[0], { selected: false })
assertComboboxOption(options[1], { selected: true })
assertComboboxOption(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 (
<Combobox value={value} onChange={setValue}>
<Combobox.Input onChange={() => {}} />
<Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options>
<Combobox.Option value="alice">alice</Combobox.Option>
<Combobox.Option value="bob">bob</Combobox.Option>
<Combobox.Option value="charlie">charlie</Combobox.Option>
</Combobox.Options>
</Combobox>
)
}

render(<Example />)

// Open combobox
await click(getComboboxButton())

// Verify that bob is the active option
assertActiveComboboxOption(getComboboxOptions()[1])
})
)

it(
'should keep the combobox open when selecting an item via the keyboard',
suppressConsoleLogs(async () => {
function Example() {
let [value, setValue] = useState<string[]>(['bob', 'charlie'])

return (
<Combobox value={value} onChange={setValue}>
<Combobox.Input onChange={() => {}} />
<Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options>
<Combobox.Option value="alice">alice</Combobox.Option>
<Combobox.Option value="bob">bob</Combobox.Option>
<Combobox.Option value="charlie">charlie</Combobox.Option>
</Combobox.Options>
</Combobox>
)
}

render(<Example />)

// Open combobox
await click(getComboboxButton())
assertCombobox({ state: ComboboxState.Visible })

// Verify that bob is the active option
await click(getComboboxOptions()[0])

// Verify that the combobox is still open
assertCombobox({ state: ComboboxState.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 (
<Combobox value={value} onChange={setValue}>
<Combobox.Input onChange={() => {}} />
<Combobox.Button>Trigger</Combobox.Button>
<Combobox.Options>
<Combobox.Option value="alice">alice</Combobox.Option>
<Combobox.Option value="bob">bob</Combobox.Option>
<Combobox.Option value="charlie">charlie</Combobox.Option>
</Combobox.Options>
</Combobox>
)
}

render(<Example />)

// Open combobox
await click(getComboboxButton())
assertCombobox({ state: ComboboxState.Visible })

let options = getComboboxOptions()

assertComboboxOption(options[0], { selected: false })
assertComboboxOption(options[1], { selected: true })
assertComboboxOption(options[2], { selected: true })

// Click on bob
await click(getComboboxOptions()[1])

assertComboboxOption(options[0], { selected: false })
assertComboboxOption(options[1], { selected: false })
assertComboboxOption(options[2], { selected: true })

// Click on bob again
await click(getComboboxOptions()[1])

assertComboboxOption(options[0], { selected: false })
assertComboboxOption(options[1], { selected: true })
assertComboboxOption(options[2], { selected: true })
})
)
})

describe('Form compatibility', () => {
it('should be possible to submit a form with a value', async () => {
let submits = jest.fn()
Expand Down
Loading

0 comments on commit 40fee45

Please sign in to comment.