Skip to content

Commit 8e558a7

Browse files
authored
Ensure the exposed activeIndex is up to date for the Combobox component (#2463)
* ensure the exposed `activeIndex` is up to date * update changelog
1 parent 4c10294 commit 8e558a7

File tree

3 files changed

+80
-1
lines changed

3 files changed

+80
-1
lines changed

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix memory leak in `Popover` component ([#2430](https://github.com/tailwindlabs/headlessui/pull/2430))
1313
- Ensure `FocusTrap` is only active when the given `enabled` value is `true` ([#2456](https://github.com/tailwindlabs/headlessui/pull/2456))
14+
- Ensure the exposed `activeIndex` is up to date for the `Combobox` component ([#2463](https://github.com/tailwindlabs/headlessui/pull/2463))
1415

1516
## [1.7.13] - 2023-04-12
1617

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4670,6 +4670,75 @@ describe('Keyboard interactions', () => {
46704670
)
46714671
})
46724672
})
4673+
4674+
it(
4675+
'should sync the active index properly',
4676+
suppressConsoleLogs(async () => {
4677+
renderTemplate({
4678+
template: html`
4679+
<Combobox v-model="value" v-slot="{ activeIndex }">
4680+
<ComboboxInput @input="filter" />
4681+
<ComboboxButton>Trigger</ComboboxButton>
4682+
<span data-test="idx">{{ activeIndex }}</span>
4683+
<ComboboxOptions>
4684+
<ComboboxOption v-for="option in options" :value="option" :key="option"
4685+
>{{ option }}</ComboboxOption
4686+
>
4687+
</ComboboxOptions>
4688+
</Combobox>
4689+
`,
4690+
setup: () => {
4691+
let value = ref(null)
4692+
let options = ref(['Option A', 'Option B', 'Option C', 'Option D'])
4693+
4694+
let query = ref('')
4695+
let filteredOptions = computed(() => {
4696+
return query.value === ''
4697+
? options.value
4698+
: options.value.filter((option) => option.includes(query.value))
4699+
})
4700+
4701+
function filter(event: Event & { target: HTMLInputElement }) {
4702+
query.value = event.target.value
4703+
}
4704+
4705+
return { value, options: filteredOptions, filter }
4706+
},
4707+
})
4708+
4709+
// Open combobox
4710+
await click(getComboboxButton())
4711+
4712+
let activeIndexEl = document.querySelector('[data-test="idx"]')
4713+
function activeIndex() {
4714+
return Number(activeIndexEl?.innerHTML)
4715+
}
4716+
4717+
expect(activeIndex()).toEqual(0)
4718+
4719+
let options: ReturnType<typeof getComboboxOptions>
4720+
4721+
await focus(getComboboxInput())
4722+
await type(word('Option B'))
4723+
4724+
// Option B should be active
4725+
options = getComboboxOptions()
4726+
expect(options[0]).toHaveTextContent('Option B')
4727+
assertActiveComboboxOption(options[0])
4728+
4729+
expect(activeIndex()).toEqual(0)
4730+
4731+
// Reveal all options again
4732+
await type(word('Option'))
4733+
4734+
// Option B should still be active
4735+
options = getComboboxOptions()
4736+
expect(options[1]).toHaveTextContent('Option B')
4737+
assertActiveComboboxOption(options[1])
4738+
4739+
expect(activeIndex()).toEqual(1)
4740+
})
4741+
)
46734742
})
46744743

46754744
describe('Mouse interactions', () => {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export let Combobox = defineComponent({
232232
) {
233233
let localActiveOptionIndex = options.value.findIndex((option) => !option.dataRef.disabled)
234234
if (localActiveOptionIndex !== -1) {
235-
return localActiveOptionIndex
235+
activeOptionIndex.value = localActiveOptionIndex
236236
}
237237
}
238238

@@ -391,6 +391,15 @@ export let Combobox = defineComponent({
391391
options.value = adjustedState.options
392392
activeOptionIndex.value = adjustedState.activeOptionIndex
393393
activationTrigger.value = ActivationTrigger.Other
394+
395+
// If some of the DOM elements aren't ready yet, then we can retry in the next tick.
396+
if (adjustedState.options.some((option) => !dom(option.dataRef.domRef))) {
397+
requestAnimationFrame(() => {
398+
let adjustedState = adjustOrderedState()
399+
options.value = adjustedState.options
400+
activeOptionIndex.value = adjustedState.activeOptionIndex
401+
})
402+
}
394403
},
395404
unregisterOption(id: string) {
396405
// When we are unregistering the currently active option, then we also have to make sure to

0 commit comments

Comments
 (0)