Skip to content

Commit b380d03

Browse files
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
1 parent 9b42daf commit b380d03

File tree

10 files changed

+217
-25
lines changed

10 files changed

+217
-25
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
1113

1214
## [1.7.16] - 2023-07-27
1315

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,50 @@ describe('Rendering', () => {
14481448
assertActiveComboboxOption(getComboboxOptions()[1])
14491449
})
14501450

1451+
it('should be possible to reset to the default value in multiple mode', async () => {
1452+
let handleSubmission = jest.fn()
1453+
let data = ['alice', 'bob', 'charlie']
1454+
1455+
render(
1456+
<form
1457+
onSubmit={(e) => {
1458+
e.preventDefault()
1459+
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
1460+
}}
1461+
>
1462+
<Combobox name="assignee" defaultValue={['bob'] as string[]} multiple>
1463+
<Combobox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Combobox.Button>
1464+
<Combobox.Options>
1465+
{data.map((person) => (
1466+
<Combobox.Option key={person} value={person}>
1467+
{person}
1468+
</Combobox.Option>
1469+
))}
1470+
</Combobox.Options>
1471+
</Combobox>
1472+
<button id="submit">submit</button>
1473+
<button type="reset" id="reset">
1474+
reset
1475+
</button>
1476+
</form>
1477+
)
1478+
1479+
await click(document.getElementById('submit'))
1480+
1481+
// Bob is the defaultValue
1482+
expect(handleSubmission).toHaveBeenLastCalledWith({
1483+
'assignee[0]': 'bob',
1484+
})
1485+
1486+
await click(document.getElementById('reset'))
1487+
await click(document.getElementById('submit'))
1488+
1489+
// Bob is still the defaultValue
1490+
expect(handleSubmission).toHaveBeenLastCalledWith({
1491+
'assignee[0]': 'bob',
1492+
})
1493+
})
1494+
14511495
it('should still call the onChange listeners when choosing new values', async () => {
14521496
let handleChange = jest.fn()
14531497

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,9 +642,9 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
642642
if (defaultValue === undefined) return
643643

644644
d.addEventListener(form.current, 'reset', () => {
645-
onChange(defaultValue)
645+
theirOnChange?.(defaultValue)
646646
})
647-
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
647+
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])
648648

649649
return (
650650
<ComboboxActionsContext.Provider value={actions}>

packages/@headlessui-react/src/components/listbox/listbox.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,50 @@ describe('Rendering', () => {
11251125
assertActiveListboxOption(getListboxOptions()[1])
11261126
})
11271127

1128+
it('should be possible to reset to the default value in multiple mode', async () => {
1129+
let handleSubmission = jest.fn()
1130+
let data = ['alice', 'bob', 'charlie']
1131+
1132+
render(
1133+
<form
1134+
onSubmit={(e) => {
1135+
e.preventDefault()
1136+
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
1137+
}}
1138+
>
1139+
<Listbox name="assignee" defaultValue={['bob'] as string[]} multiple>
1140+
<Listbox.Button>{({ value }) => value.join(', ') || 'Trigger'}</Listbox.Button>
1141+
<Listbox.Options>
1142+
{data.map((person) => (
1143+
<Listbox.Option key={person} value={person}>
1144+
{person}
1145+
</Listbox.Option>
1146+
))}
1147+
</Listbox.Options>
1148+
</Listbox>
1149+
<button id="submit">submit</button>
1150+
<button type="reset" id="reset">
1151+
reset
1152+
</button>
1153+
</form>
1154+
)
1155+
1156+
await click(document.getElementById('submit'))
1157+
1158+
// Bob is the defaultValue
1159+
expect(handleSubmission).toHaveBeenLastCalledWith({
1160+
'assignee[0]': 'bob',
1161+
})
1162+
1163+
await click(document.getElementById('reset'))
1164+
await click(document.getElementById('submit'))
1165+
1166+
// Bob is still the defaultValue
1167+
expect(handleSubmission).toHaveBeenLastCalledWith({
1168+
'assignee[0]': 'bob',
1169+
})
1170+
})
1171+
11281172
it('should still call the onChange listeners when choosing new values', async () => {
11291173
let handleChange = jest.fn()
11301174

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,9 @@ function ListboxFn<
537537
if (defaultValue === undefined) return
538538

539539
d.addEventListener(form.current, 'reset', () => {
540-
onChange(defaultValue)
540+
theirOnChange?.(defaultValue)
541541
})
542-
}, [form, onChange /* Explicitly ignoring `defaultValue` */])
542+
}, [form, theirOnChange /* Explicitly ignoring `defaultValue` */])
543543

544544
return (
545545
<ListboxActionsContext.Provider value={actions}>

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Fix form elements for uncontrolled `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
13+
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
1114

1215
## [1.7.15] - 2023-07-27
1316

packages/@headlessui-vue/src/components/combobox/combobox.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,52 @@ describe('Rendering', () => {
15531553
})
15541554
)
15551555

1556+
it('should be possible to reset to the default value in multiple mode', async () => {
1557+
let data = ['alice', 'bob', 'charlie']
1558+
let handleSubmission = jest.fn()
1559+
1560+
renderTemplate({
1561+
template: html`
1562+
<form @submit="handleSubmit">
1563+
<Combobox name="assignee" :defaultValue="['bob']" multiple>
1564+
<ComboboxButton v-slot="{ value }"
1565+
>{{ value.join(', ') || 'Trigger' }}</ComboboxButton
1566+
>
1567+
<ComboboxOptions>
1568+
<ComboboxOption v-for="person in data" :key="person" :value="person">
1569+
{{ person }}
1570+
</ComboboxOption>
1571+
</ComboboxOptions>
1572+
</Combobox>
1573+
<button id="submit">submit</button>
1574+
<button type="reset" id="reset">reset</button>
1575+
</form>
1576+
`,
1577+
setup: () => ({
1578+
data,
1579+
handleSubmit(e: SubmitEvent) {
1580+
e.preventDefault()
1581+
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
1582+
},
1583+
}),
1584+
})
1585+
1586+
await click(document.getElementById('submit'))
1587+
1588+
// Bob is the defaultValue
1589+
expect(handleSubmission).toHaveBeenLastCalledWith({
1590+
'assignee[0]': 'bob',
1591+
})
1592+
1593+
await click(document.getElementById('reset'))
1594+
await click(document.getElementById('submit'))
1595+
1596+
// Bob is still the defaultValue
1597+
expect(handleSubmission).toHaveBeenLastCalledWith({
1598+
'assignee[0]': 'bob',
1599+
})
1600+
})
1601+
15561602
it('should still call the onChange listeners when choosing new values', async () => {
15571603
let handleChange = jest.fn()
15581604

packages/@headlessui-vue/src/components/combobox/combobox.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,21 @@ export let Combobox = defineComponent({
189189

190190
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
191191
let nullable = computed(() => props.nullable)
192-
let [value, theirOnChange] = useControllable(
193-
computed(() =>
194-
props.modelValue === undefined
195-
? match(mode.value, {
196-
[ValueMode.Multi]: [],
197-
[ValueMode.Single]: undefined,
198-
})
199-
: props.modelValue
200-
),
192+
let [directValue, theirOnChange] = useControllable(
193+
computed(() => props.modelValue),
201194
(value: unknown) => emit('update:modelValue', value),
202195
computed(() => props.defaultValue)
203196
)
204197

198+
let value = computed(() =>
199+
directValue.value === undefined
200+
? match(mode.value, {
201+
[ValueMode.Multi]: [],
202+
[ValueMode.Single]: undefined,
203+
})
204+
: directValue.value
205+
)
206+
205207
let goToOptionRaf: ReturnType<typeof requestAnimationFrame> | null = null
206208
let orderOptionsRaf: ReturnType<typeof requestAnimationFrame> | null = null
207209

packages/@headlessui-vue/src/components/listbox/listbox.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,50 @@ describe('Rendering', () => {
12361236
})
12371237
)
12381238

1239+
it('should be possible to reset to the default value in multiple mode', async () => {
1240+
let data = ['alice', 'bob', 'charlie']
1241+
let handleSubmission = jest.fn()
1242+
1243+
renderTemplate({
1244+
template: html`
1245+
<form @submit="handleSubmit">
1246+
<Listbox name="assignee" :defaultValue="['bob']" multiple>
1247+
<ListboxButton v-slot="{ value }">{{ value.join(', ') || 'Trigger' }}</ListboxButton>
1248+
<ListboxOptions>
1249+
<ListboxOption v-for="person in data" :key="person" :value="person">
1250+
{{ person }}
1251+
</ListboxOption>
1252+
</ListboxOptions>
1253+
</Listbox>
1254+
<button id="submit">submit</button>
1255+
<button type="reset" id="reset">reset</button>
1256+
</form>
1257+
`,
1258+
setup: () => ({
1259+
data,
1260+
handleSubmit(e: SubmitEvent) {
1261+
e.preventDefault()
1262+
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
1263+
},
1264+
}),
1265+
})
1266+
1267+
await click(document.getElementById('submit'))
1268+
1269+
// Bob is the defaultValue
1270+
expect(handleSubmission).toHaveBeenLastCalledWith({
1271+
'assignee[0]': 'bob',
1272+
})
1273+
1274+
await click(document.getElementById('reset'))
1275+
await click(document.getElementById('submit'))
1276+
1277+
// Bob is still the defaultValue
1278+
expect(handleSubmission).toHaveBeenLastCalledWith({
1279+
'assignee[0]': 'bob',
1280+
})
1281+
})
1282+
12391283
it('should still call the onChange listeners when choosing new values', async () => {
12401284
let handleChange = jest.fn()
12411285

packages/@headlessui-vue/src/components/listbox/listbox.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,19 +181,22 @@ export let Listbox = defineComponent({
181181
}
182182

183183
let mode = computed(() => (props.multiple ? ValueMode.Multi : ValueMode.Single))
184-
let [value, theirOnChange] = useControllable(
185-
computed(() =>
186-
props.modelValue === undefined
187-
? match(mode.value, {
188-
[ValueMode.Multi]: [],
189-
[ValueMode.Single]: undefined,
190-
})
191-
: props.modelValue
192-
),
184+
185+
let [directValue, theirOnChange] = useControllable(
186+
computed(() => props.modelValue),
193187
(value: unknown) => emit('update:modelValue', value),
194188
computed(() => props.defaultValue)
195189
)
196190

191+
let value = computed(() =>
192+
directValue.value === undefined
193+
? match(mode.value, {
194+
[ValueMode.Multi]: [],
195+
[ValueMode.Single]: undefined,
196+
})
197+
: directValue.value
198+
)
199+
197200
let api = {
198201
listboxState,
199202
value,
@@ -300,6 +303,10 @@ export let Listbox = defineComponent({
300303
activeOptionIndex.value = adjustedState.activeOptionIndex
301304
activationTrigger.value = ActivationTrigger.Other
302305
},
306+
theirOnChange(value: unknown) {
307+
if (props.disabled) return
308+
theirOnChange(value)
309+
},
303310
select(value: unknown) {
304311
if (props.disabled) return
305312
theirOnChange(
@@ -357,7 +364,7 @@ export let Listbox = defineComponent({
357364
if (props.defaultValue === undefined) return
358365

359366
function handle() {
360-
api.select(props.defaultValue)
367+
api.theirOnChange(props.defaultValue)
361368
}
362369

363370
form.value.addEventListener('reset', handle)

0 commit comments

Comments
 (0)