Skip to content

Commit

Permalink
Expose the value from the Combobox and Listbox components rende…
Browse files Browse the repository at this point in the history
…r prop (#1822)

* expose the `value` for `Combobox` and `Listbox`

* update changelog
  • Loading branch information
RobinMalfait committed Sep 5, 2022
1 parent 3db54db commit 1fd8cfc
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure enter transitions work when using `unmount={false}` ([#1811](https://github.com/tailwindlabs/headlessui/pull/1811))
- Improve accessibility when announcing `Listbox.Option` and `Combobox.Option` components ([#1812](https://github.com/tailwindlabs/headlessui/pull/1812))
- Fix `ref` stealing from children ([#1820](https://github.com/tailwindlabs/headlessui/pull/1820))
- Expose the `value` from the `Combobox` and `Listbox` components render prop ([#1822](https://github.com/tailwindlabs/headlessui/pull/1822))

## [1.6.6] - 2022-07-07

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: false, disabled: false }),
textContent: JSON.stringify({ open: false, disabled: false, value: 'test' }),
})
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })

Expand All @@ -693,7 +693,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.Visible,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: true, disabled: false }),
textContent: JSON.stringify({ open: true, disabled: false, value: 'test' }),
})
assertComboboxList({ state: ComboboxState.Visible })
})
Expand All @@ -719,7 +719,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: false, disabled: false }),
textContent: JSON.stringify({ open: false, disabled: false, value: 'test' }),
})
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })

Expand All @@ -728,7 +728,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.Visible,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: true, disabled: false }),
textContent: JSON.stringify({ open: true, disabled: false, value: 'test' }),
})
assertComboboxList({ state: ComboboxState.Visible })
})
Expand Down Expand Up @@ -1036,6 +1036,75 @@ describe('Rendering', () => {
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)))
}}
>
<Combobox name="assignee">
{({ value }) => (
<>
<div data-testid="value">{value}</div>
<Combobox.Input onChange={NOOP} />
<Combobox.Button>
{({ value }) => (
<>
Trigger
<div data-testid="value-2">{value}</div>
</>
)}
</Combobox.Button>
<Combobox.Options>
<Combobox.Option value="alice">Alice</Combobox.Option>
<Combobox.Option value="bob">Bob</Combobox.Option>
<Combobox.Option value="charlie">Charlie</Combobox.Option>
</Combobox.Options>
</>
)}
</Combobox>
<button id="submit">submit</button>
</form>
)

await click(document.getElementById('submit'))

// No values
expect(handleSubmission).toHaveBeenLastCalledWith({})

// Open combobox
await click(getComboboxButton())

// Choose alice
await click(getComboboxOptions()[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 combobox
await click(getComboboxButton())

// Choose charlie
await click(getComboboxOptions()[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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@ let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
disabled: boolean
value: any
}
type ButtonPropsWeControl =
| 'id'
Expand Down Expand Up @@ -896,7 +897,11 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
}, [data.labelRef.current, id])

let slot = useMemo<ButtonRenderPropArg>(
() => ({ open: data.comboboxState === ComboboxState.Open, disabled: data.disabled }),
() => ({
open: data.comboboxState === ComboboxState.Open,
disabled: data.disabled,
value: data.value,
}),
[data]
)
let theirProps = props
Expand Down
68 changes: 68 additions & 0 deletions packages/@headlessui-react/src/components/listbox/listbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,74 @@ describe('Rendering', () => {
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()

Expand Down
11 changes: 8 additions & 3 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,10 @@ function stateReducer(state: StateDefinition, action: Actions) {
// ---

let DEFAULT_LISTBOX_TAG = Fragment
interface ListboxRenderPropArg<TType> {
interface ListboxRenderPropArg<T> {
open: boolean
disabled: boolean
value: TType
value: T
}

let ListboxRoot = forwardRefWithAs(function Listbox<
Expand Down Expand Up @@ -461,6 +461,7 @@ let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
disabled: boolean
value: any
}
type ButtonPropsWeControl =
| 'id'
Expand Down Expand Up @@ -537,7 +538,11 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
}, [state.labelRef.current, id])

let slot = useMemo<ButtonRenderPropArg>(
() => ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }),
() => ({
open: state.listboxState === ListboxStates.Open,
disabled: state.disabled,
value: state.propsRef.current.value,
}),
[state]
)
let theirProps = props
Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Only restore focus to the `MenuButton` if necessary when activating a `MenuOption` ([#1782](https://github.com/tailwindlabs/headlessui/pull/1782))
- Don't scroll when wrapping around in focus trap ([#1789](https://github.com/tailwindlabs/headlessui/pull/1789))
- Improve accessibility when announcing `ListboxOption` and `ComboboxOption` components ([#1812](https://github.com/tailwindlabs/headlessui/pull/1812))
- Expose the `value` from the `Combobox` and `Listbox` components slot ([#1822](https://github.com/tailwindlabs/headlessui/pull/1822))

## [1.6.7] - 2022-07-12

Expand Down
72 changes: 68 additions & 4 deletions packages/@headlessui-vue/src/components/combobox/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: false, disabled: false }),
textContent: JSON.stringify({ open: false, disabled: false, value: null }),
})
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })

Expand All @@ -722,7 +722,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.Visible,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: true, disabled: false }),
textContent: JSON.stringify({ open: true, disabled: false, value: null }),
})
assertComboboxList({ state: ComboboxState.Visible })
})
Expand Down Expand Up @@ -751,7 +751,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: false, disabled: false }),
textContent: JSON.stringify({ open: false, disabled: false, value: null }),
})
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })

Expand All @@ -760,7 +760,7 @@ describe('Rendering', () => {
assertComboboxButton({
state: ComboboxState.Visible,
attributes: { id: 'headlessui-combobox-button-2' },
textContent: JSON.stringify({ open: true, disabled: false }),
textContent: JSON.stringify({ open: true, disabled: false, value: null }),
})
assertComboboxList({ state: ComboboxState.Visible })
})
Expand Down Expand Up @@ -1125,6 +1125,70 @@ describe('Rendering', () => {
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'charlie' })
})

it('should expose the value via the render prop', async () => {
let handleSubmission = jest.fn()

renderTemplate({
template: html`
<form @submit="handleSubmit">
<Combobox name="assignee" v-slot="{ value }">
<div data-testid="value">{{value}}</div>
<ComboboxInput />
<ComboboxButton v-slot="{ value }">
Trigger
<div data-testid="value-2">{{value}}</div>
</ComboboxButton>
<ComboboxOptions>
<ComboboxOption value="alice">Alice</ComboboxOption>
<ComboboxOption value="bob">Bob</ComboboxOption>
<ComboboxOption value="charlie">Charlie</ComboboxOption>
</ComboboxOptions>
</Combobox>
<button id="submit">submit</button>
</form>
`,
setup: () => ({
handleSubmit(e: SubmitEvent) {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
},
}),
})

await click(document.getElementById('submit'))

// No values
expect(handleSubmission).toHaveBeenLastCalledWith({})

// Open combobox
await click(getComboboxButton())

// Choose alice
await click(getComboboxOptions()[0])
expect(document.querySelector('[data-testid="value"]')).toHaveTextContent('alice')
expect(document.querySelector('[data-testid="value-2"]')).toHaveTextContent('alice')

// Submit
await click(document.getElementById('submit'))

// Alice should be submitted
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'alice' })

// Open combobox
await click(getComboboxButton())

// Choose charlie
await click(getComboboxOptions()[2])
expect(document.querySelector('[data-testid="value"]')).toHaveTextContent('charlie')
expect(document.querySelector('[data-testid="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()

Expand Down
2 changes: 2 additions & 0 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export let Combobox = defineComponent({
disabled,
activeIndex: api.activeOptionIndex.value,
activeOption: activeOption.value,
value: value.value,
}

return h(Fragment, [
Expand Down Expand Up @@ -563,6 +564,7 @@ export let ComboboxButton = defineComponent({
let slot = {
open: api.comboboxState.value === ComboboxStates.Open,
disabled: api.disabled.value,
value: api.value.value,
}
let ourProps = {
ref: api.buttonRef,
Expand Down
Loading

0 comments on commit 1fd8cfc

Please sign in to comment.