diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 4c69e2df21..b27cc57231 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -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
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
index 05946bfa17..cc67bc71bf 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
@@ -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 })
@@ -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 })
})
@@ -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 })
@@ -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 })
})
@@ -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(
+
+ )
+
+ 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()
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index 9e03a76d5d..c2c1338838 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -815,6 +815,7 @@ let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
disabled: boolean
+ value: any
}
type ButtonPropsWeControl =
| 'id'
@@ -896,7 +897,11 @@ let Button = forwardRefWithAs(function Button(
- () => ({ open: data.comboboxState === ComboboxState.Open, disabled: data.disabled }),
+ () => ({
+ open: data.comboboxState === ComboboxState.Open,
+ disabled: data.disabled,
+ value: data.value,
+ }),
[data]
)
let theirProps = props
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
index 1cb824903c..94742c0bdd 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.test.tsx
@@ -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(
+
+ )
+
+ 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()
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 2cfcf032fc..6d9250daab 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -299,10 +299,10 @@ function stateReducer(state: StateDefinition, action: Actions) {
// ---
let DEFAULT_LISTBOX_TAG = Fragment
-interface ListboxRenderPropArg {
+interface ListboxRenderPropArg {
open: boolean
disabled: boolean
- value: TType
+ value: T
}
let ListboxRoot = forwardRefWithAs(function Listbox<
@@ -461,6 +461,7 @@ let DEFAULT_BUTTON_TAG = 'button' as const
interface ButtonRenderPropArg {
open: boolean
disabled: boolean
+ value: any
}
type ButtonPropsWeControl =
| 'id'
@@ -537,7 +538,11 @@ let Button = forwardRefWithAs(function Button(
- () => ({ 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
diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md
index cd7fdf2af2..12ec43a78a 100644
--- a/packages/@headlessui-vue/CHANGELOG.md
+++ b/packages/@headlessui-vue/CHANGELOG.md
@@ -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
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index 9b307d5eab..014c15ed55 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -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 })
@@ -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 })
})
@@ -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 })
@@ -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 })
})
@@ -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`
+
+ `,
+ 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()
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts
index 159a8d3b6c..b9c73b7127 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts
@@ -413,6 +413,7 @@ export let Combobox = defineComponent({
disabled,
activeIndex: api.activeOptionIndex.value,
activeOption: activeOption.value,
+ value: value.value,
}
return h(Fragment, [
@@ -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,
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
index c09f101f52..b9eb47d135 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.test.tsx
@@ -556,7 +556,7 @@ describe('Rendering', () => {
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
- textContent: JSON.stringify({ open: false, disabled: false }),
+ textContent: JSON.stringify({ open: false, disabled: false, value: null }),
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
@@ -565,7 +565,7 @@ describe('Rendering', () => {
assertListboxButton({
state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
- textContent: JSON.stringify({ open: true, disabled: false }),
+ textContent: JSON.stringify({ open: true, disabled: false, value: null }),
})
assertListbox({ state: ListboxState.Visible })
})
@@ -593,7 +593,7 @@ describe('Rendering', () => {
assertListboxButton({
state: ListboxState.InvisibleUnmounted,
attributes: { id: 'headlessui-listbox-button-1' },
- textContent: JSON.stringify({ open: false, disabled: false }),
+ textContent: JSON.stringify({ open: false, disabled: false, value: null }),
})
assertListbox({ state: ListboxState.InvisibleUnmounted })
@@ -602,7 +602,7 @@ describe('Rendering', () => {
assertListboxButton({
state: ListboxState.Visible,
attributes: { id: 'headlessui-listbox-button-1' },
- textContent: JSON.stringify({ open: true, disabled: false }),
+ textContent: JSON.stringify({ open: true, disabled: false, value: null }),
})
assertListbox({ state: ListboxState.Visible })
})
@@ -952,6 +952,69 @@ describe('Rendering', () => {
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'charlie' })
})
+ it('should expose the value via the render prop', async () => {
+ let handleSubmission = jest.fn()
+
+ renderTemplate({
+ template: html`
+
+ `,
+ 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 listbox
+ await click(getListboxButton())
+
+ // Choose alice
+ await click(getListboxOptions()[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 listbox
+ await click(getListboxButton())
+
+ // Choose charlie
+ await click(getListboxOptions()[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()
diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts
index 44cb3b1c0a..70bacc15c2 100644
--- a/packages/@headlessui-vue/src/components/listbox/listbox.ts
+++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts
@@ -330,7 +330,7 @@ export let Listbox = defineComponent({
return () => {
let { name, modelValue, disabled, ...theirProps } = props
- let slot = { open: listboxState.value === ListboxStates.Open, disabled }
+ let slot = { open: listboxState.value === ListboxStates.Open, disabled, value: value.value }
return h(Fragment, [
...(name != null && value.value != null
@@ -475,7 +475,9 @@ export let ListboxButton = defineComponent({
let slot = {
open: api.listboxState.value === ListboxStates.Open,
disabled: api.disabled.value,
+ value: api.value.value,
}
+
let ourProps = {
ref: api.buttonRef,
id,