From 8e558a7087c324914da5e997ffb485a9cdfbd97d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 28 Apr 2023 15:11:18 +0200 Subject: [PATCH] Ensure the exposed `activeIndex` is up to date for the `Combobox` component (#2463) * ensure the exposed `activeIndex` is up to date * update changelog --- packages/@headlessui-vue/CHANGELOG.md | 1 + .../src/components/combobox/combobox.test.ts | 69 +++++++++++++++++++ .../src/components/combobox/combobox.ts | 11 ++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index c5d04a6..acd57d3 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix memory leak in `Popover` component ([#2430](https://github.com/tailwindlabs/headlessui/pull/2430)) - Ensure `FocusTrap` is only active when the given `enabled` value is `true` ([#2456](https://github.com/tailwindlabs/headlessui/pull/2456)) +- Ensure the exposed `activeIndex` is up to date for the `Combobox` component ([#2463](https://github.com/tailwindlabs/headlessui/pull/2463)) ## [1.7.13] - 2023-04-12 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts index 3c066fb..1306252 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts @@ -4670,6 +4670,75 @@ describe('Keyboard interactions', () => { ) }) }) + + it( + 'should sync the active index properly', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + + Trigger + {{ activeIndex }} + + {{ option }} + + + `, + setup: () => { + let value = ref(null) + let options = ref(['Option A', 'Option B', 'Option C', 'Option D']) + + let query = ref('') + let filteredOptions = computed(() => { + return query.value === '' + ? options.value + : options.value.filter((option) => option.includes(query.value)) + }) + + function filter(event: Event & { target: HTMLInputElement }) { + query.value = event.target.value + } + + return { value, options: filteredOptions, filter } + }, + }) + + // Open combobox + await click(getComboboxButton()) + + let activeIndexEl = document.querySelector('[data-test="idx"]') + function activeIndex() { + return Number(activeIndexEl?.innerHTML) + } + + expect(activeIndex()).toEqual(0) + + let options: ReturnType + + await focus(getComboboxInput()) + await type(word('Option B')) + + // Option B should be active + options = getComboboxOptions() + expect(options[0]).toHaveTextContent('Option B') + assertActiveComboboxOption(options[0]) + + expect(activeIndex()).toEqual(0) + + // Reveal all options again + await type(word('Option')) + + // Option B should still be active + options = getComboboxOptions() + expect(options[1]).toHaveTextContent('Option B') + assertActiveComboboxOption(options[1]) + + expect(activeIndex()).toEqual(1) + }) + ) }) describe('Mouse interactions', () => { diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index c0f26aa..7cf476e 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -232,7 +232,7 @@ export let Combobox = defineComponent({ ) { let localActiveOptionIndex = options.value.findIndex((option) => !option.dataRef.disabled) if (localActiveOptionIndex !== -1) { - return localActiveOptionIndex + activeOptionIndex.value = localActiveOptionIndex } } @@ -391,6 +391,15 @@ export let Combobox = defineComponent({ options.value = adjustedState.options activeOptionIndex.value = adjustedState.activeOptionIndex activationTrigger.value = ActivationTrigger.Other + + // If some of the DOM elements aren't ready yet, then we can retry in the next tick. + if (adjustedState.options.some((option) => !dom(option.dataRef.domRef))) { + requestAnimationFrame(() => { + let adjustedState = adjustOrderedState() + options.value = adjustedState.options + activeOptionIndex.value = adjustedState.activeOptionIndex + }) + } }, unregisterOption(id: string) { // When we are unregistering the currently active option, then we also have to make sure to