Use correct value when resetting <Listbox multiple> and <Combobox multiple> (#2626)
* Fix bug with non-controlled, multiple combobox in Vue It thought it was always controlled which broke things * Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` * Update changelog
This commit is contained in:
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Fixed
|
||||
|
||||
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
|
||||
|
||||
## [1.7.16] - 2023-07-27
|
||||
|
||||
|
||||
@@ -1448,6 +1448,50 @@ describe('Rendering', () => {
|
||||
assertActiveComboboxOption(getComboboxOptions()[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)))
|
||||
}}
|
||||
>
|
||||
<Combobox name="assignee" defaultValue={['bob'] as string[]} multiple>
|
||||
<Combobox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
{data.map((person) => (
|
||||
<Combobox.Option key={person} value={person}>
|
||||
{person}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
<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()
|
||||
|
||||
|
||||
@@ -642,9 +642,9 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
|
||||
if (defaultValue === undefined) return
|
||||
|
||||
d.addEventListener(form.current, 'reset', () => {
|
||||
onChange(defaultValue)
|
||||
theirOnChange?.(defaultValue)
|
||||
})
|
||||
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
|
||||
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])
|
||||
|
||||
return (
|
||||
<ComboboxActionsContext.Provider value={actions}>
|
||||
|
||||
@@ -1125,6 +1125,50 @@ describe('Rendering', () => {
|
||||
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()
|
||||
|
||||
|
||||
@@ -537,9 +537,9 @@ function ListboxFn<
|
||||
if (defaultValue === undefined) return
|
||||
|
||||
d.addEventListener(form.current, 'reset', () => {
|
||||
onChange(defaultValue)
|
||||
theirOnChange?.(defaultValue)
|
||||
})
|
||||
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
|
||||
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])
|
||||
|
||||
return (
|
||||
<ListboxActionsContext.Provider value={actions}>
|
||||
|
||||
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Fixed
|
||||
|
||||
- Fix form elements for uncontrolled `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
|
||||
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
|
||||
|
||||
## [1.7.15] - 2023-07-27
|
||||
|
||||
|
||||
@@ -1553,6 +1553,52 @@ describe('Rendering', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it('should be possible to reset to the default value in multiple mode', async () => {
|
||||
let data = ['alice', 'bob', 'charlie']
|
||||
let handleSubmission = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<Combobox name="assignee" :defaultValue="['bob']" multiple>
|
||||
<ComboboxButton v-slot="{ value }"
|
||||
>{{ value.join(', ') || 'Trigger' }}</ComboboxButton
|
||||
>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption v-for="person in data" :key="person" :value="person">
|
||||
{{ person }}
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
<button id="submit">submit</button>
|
||||
<button type="reset" id="reset">reset</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => ({
|
||||
data,
|
||||
handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -189,19 +189,21 @@ export let Combobox = defineComponent({
|
||||
|
||||
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
|
||||
let nullable = computed(() => props.nullable)
|
||||
let [value, theirOnChange] = useControllable(
|
||||
computed(() =>
|
||||
props.modelValue === undefined
|
||||
? match(mode.value, {
|
||||
[ValueMode.Multi]: [],
|
||||
[ValueMode.Single]: undefined,
|
||||
})
|
||||
: props.modelValue
|
||||
),
|
||||
let [directValue, theirOnChange] = useControllable(
|
||||
computed(() => props.modelValue),
|
||||
(value: unknown) => emit('update:modelValue', value),
|
||||
computed(() => props.defaultValue)
|
||||
)
|
||||
|
||||
let value = computed(() =>
|
||||
directValue.value === undefined
|
||||
? match(mode.value, {
|
||||
[ValueMode.Multi]: [],
|
||||
[ValueMode.Single]: undefined,
|
||||
})
|
||||
: directValue.value
|
||||
)
|
||||
|
||||
let goToOptionRaf: ReturnType<typeof requestAnimationFrame> | null = null
|
||||
let orderOptionsRaf: ReturnType<typeof requestAnimationFrame> | null = null
|
||||
|
||||
|
||||
@@ -1236,6 +1236,50 @@ describe('Rendering', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it('should be possible to reset to the default value in multiple mode', async () => {
|
||||
let data = ['alice', 'bob', 'charlie']
|
||||
let handleSubmission = jest.fn()
|
||||
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<form @submit="handleSubmit">
|
||||
<Listbox name="assignee" :defaultValue="['bob']" multiple>
|
||||
<ListboxButton v-slot="{ value }">{{ value.join(', ') || 'Trigger' }}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
<ListboxOption v-for="person in data" :key="person" :value="person">
|
||||
{{ person }}
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
<button id="submit">submit</button>
|
||||
<button type="reset" id="reset">reset</button>
|
||||
</form>
|
||||
`,
|
||||
setup: () => ({
|
||||
data,
|
||||
handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -181,19 +181,22 @@ export let Listbox = defineComponent({
|
||||
}
|
||||
|
||||
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
|
||||
let [value, theirOnChange] = useControllable(
|
||||
computed(() =>
|
||||
props.modelValue === undefined
|
||||
? match(mode.value, {
|
||||
[ValueMode.Multi]: [],
|
||||
[ValueMode.Single]: undefined,
|
||||
})
|
||||
: props.modelValue
|
||||
),
|
||||
|
||||
let [directValue, theirOnChange] = useControllable(
|
||||
computed(() => props.modelValue),
|
||||
(value: unknown) => emit('update:modelValue', value),
|
||||
computed(() => props.defaultValue)
|
||||
)
|
||||
|
||||
let value = computed(() =>
|
||||
directValue.value === undefined
|
||||
? match(mode.value, {
|
||||
[ValueMode.Multi]: [],
|
||||
[ValueMode.Single]: undefined,
|
||||
})
|
||||
: directValue.value
|
||||
)
|
||||
|
||||
let api = {
|
||||
listboxState,
|
||||
value,
|
||||
@@ -300,6 +303,10 @@ export let Listbox = defineComponent({
|
||||
activeOptionIndex.value = adjustedState.activeOptionIndex
|
||||
activationTrigger.value = ActivationTrigger.Other
|
||||
},
|
||||
theirOnChange(value: unknown) {
|
||||
if (props.disabled) return
|
||||
theirOnChange(value)
|
||||
},
|
||||
select(value: unknown) {
|
||||
if (props.disabled) return
|
||||
theirOnChange(
|
||||
@@ -357,7 +364,7 @@ export let Listbox = defineComponent({
|
||||
if (props.defaultValue === undefined) return
|
||||
|
||||
function handle() {
|
||||
api.select(props.defaultValue)
|
||||
api.theirOnChange(props.defaultValue)
|
||||
}
|
||||
|
||||
form.value.addEventListener('reset', handle)
|
||||
|
||||
Reference in New Issue
Block a user