From 5f7faf0861599938c8145909119a51a81493475c Mon Sep 17 00:00:00 2001 From: yanuaraditia Date: Tue, 12 May 2026 23:53:25 +0800 Subject: [PATCH 1/3] fix(Popper): memoize content subtree updates --- packages/core/src/Combobox/Combobox.test.ts | 64 ++++++++++++++++++++- packages/core/src/Popper/PopperContent.vue | 1 + 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Combobox/Combobox.test.ts b/packages/core/src/Combobox/Combobox.test.ts index 62dfbd49e8..6880c5da54 100644 --- a/packages/core/src/Combobox/Combobox.test.ts +++ b/packages/core/src/Combobox/Combobox.test.ts @@ -2,8 +2,9 @@ import type { DOMWrapper, VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { axe } from 'vitest-axe' -import { nextTick } from 'vue' +import { defineComponent, h, nextTick, ref } from 'vue' import { handleSubmit, sleep } from '@/test' +import { ComboboxAnchor, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxRoot, ComboboxTrigger, ComboboxViewport } from '.' import Combobox from './story/_Combobox.vue' import ComboboxObject from './story/_ComboboxObject.vue' import ComboboxTagsInput from './story/_ComboboxTagsInput.vue' @@ -458,3 +459,64 @@ describe('given Combobox with TagsInput and addOnBlur', () => { expect(document.activeElement).toBe(input.element) }) }) + +describe('comboboxContent with popper positioning', () => { + const getSlotRenderCount = vi.fn(() => ({ value: 0 })) + + const PopperCombobox = defineComponent({ + setup() { + const modelValue = ref('') + const slotRenderCount = getSlotRenderCount() + const options = Array.from({ length: 60 }, (_, index) => `Option ${index}`) + + return () => h(ComboboxRoot, { + 'modelValue': modelValue.value, + 'onUpdate:modelValue': (value: string) => modelValue.value = value, + }, { + default: () => [ + h(ComboboxAnchor, null, { + default: () => [ + h(ComboboxInput), + h(ComboboxTrigger, null, { default: () => 'Open' }), + ], + }), + h(ComboboxContent, { position: 'popper' }, { + default: () => { + slotRenderCount.value += 1 + return h(ComboboxViewport, null, { + default: () => options.map(option => h(ComboboxItem, { key: option, value: option }, { default: () => option })), + }) + }, + }), + ], + }) + }, + }) + + beforeEach(() => { + document.body.innerHTML = '' + getSlotRenderCount.mockClear() + globalThis.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + } + }) + + it('does not rerender option slot content after popper position updates', async () => { + const slotRenderCount = { value: 0 } + getSlotRenderCount.mockReturnValue(slotRenderCount) + + const wrapper = mount(PopperCombobox, { attachTo: document.body }) + + await wrapper.find('button').trigger('click') + await nextTick() + + expect(slotRenderCount.value).toBeLessThanOrEqual(2) + + await sleep(0) + await nextTick() + + expect(slotRenderCount.value).toBeLessThanOrEqual(2) + }) +}) diff --git a/packages/core/src/Popper/PopperContent.vue b/packages/core/src/Popper/PopperContent.vue index b7e7b16230..04f5b657a3 100644 --- a/packages/core/src/Popper/PopperContent.vue +++ b/packages/core/src/Popper/PopperContent.vue @@ -403,6 +403,7 @@ providePopperContentContext({ > Date: Tue, 12 May 2026 23:25:27 +0700 Subject: [PATCH 2/3] fix: undefined variable `as` should be `props.as` --- packages/core/src/Popper/PopperContent.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/Popper/PopperContent.vue b/packages/core/src/Popper/PopperContent.vue index 04f5b657a3..efe8a868f5 100644 --- a/packages/core/src/Popper/PopperContent.vue +++ b/packages/core/src/Popper/PopperContent.vue @@ -403,7 +403,7 @@ providePopperContentContext({ > Date: Tue, 2 Jun 2026 10:29:11 +0800 Subject: [PATCH 3/3] fix: scope popper memoization to combobox --- packages/core/src/Combobox/Combobox.test.ts | 17 +++++++++++++---- packages/core/src/Combobox/ComboboxItem.vue | 2 +- packages/core/src/Popper/PopperContent.vue | 20 +++++++++++++++++++- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/core/src/Combobox/Combobox.test.ts b/packages/core/src/Combobox/Combobox.test.ts index 58096efa51..874ba4be6c 100644 --- a/packages/core/src/Combobox/Combobox.test.ts +++ b/packages/core/src/Combobox/Combobox.test.ts @@ -581,7 +581,17 @@ describe('comboboxContent with popper positioning', () => { document.body.innerHTML = '' getSlotRenderCount.mockClear() globalThis.ResizeObserver = class ResizeObserver { - observe() {} + private callback: ResizeObserverCallback + + constructor(callback: ResizeObserverCallback) { + this.callback = callback + } + + observe(target: Element) { + this.callback([{ target } as ResizeObserverEntry], this) + this.callback([{ target } as ResizeObserverEntry], this) + } + unobserve() {} disconnect() {} } @@ -596,13 +606,12 @@ describe('comboboxContent with popper positioning', () => { await wrapper.find('button').trigger('click') await nextTick() - const renderCountAfterOpen = slotRenderCount.value - expect(renderCountAfterOpen).toBeLessThanOrEqual(4) + expect(slotRenderCount.value).toBe(3) await sleep(0) await nextTick() - expect(slotRenderCount.value).toBeLessThanOrEqual(4) + expect(slotRenderCount.value).toBe(4) }) it('updates visible options when filtering while popper content is open', async () => { diff --git a/packages/core/src/Combobox/ComboboxItem.vue b/packages/core/src/Combobox/ComboboxItem.vue index 35c8c4e0e8..a39daf2bd0 100644 --- a/packages/core/src/Combobox/ComboboxItem.vue +++ b/packages/core/src/Combobox/ComboboxItem.vue @@ -80,7 +80,7 @@ onUnmounted(() => { v-bind="props" :id="id" ref="primitiveElement" - v-memo="[isRender, rootContext.disabled.value, disabled, props.value, props.as, props.asChild, ...Object.values($attrs)]" + v-memo="[isRender, rootContext.filterSearch.value, rootContext.disabled.value, disabled, props.value, props.as, props.asChild, ...Object.values($attrs)]" :disabled="rootContext.disabled.value || disabled" @select="(event) => { emits('select', event as any) diff --git a/packages/core/src/Popper/PopperContent.vue b/packages/core/src/Popper/PopperContent.vue index 6a824b0691..0a6cc2b038 100644 --- a/packages/core/src/Popper/PopperContent.vue +++ b/packages/core/src/Popper/PopperContent.vue @@ -407,6 +407,7 @@ providePopperContentContext({ }" > + + + +