Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose the value from the Combobox and Listbox components render prop #1822

Merged
merged 2 commits into from Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
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
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
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
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
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
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
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
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