From ca3430fa7df18d2ebd2e43342ca4f5106d15931e Mon Sep 17 00:00:00 2001 From: John Chamver Puno Date: Fri, 12 Sep 2025 21:53:49 +0800 Subject: [PATCH 1/8] fix: vcombobox empty string initial value not showing placeholder --- packages/vuetify/src/components/VCombobox/VCombobox.tsx | 7 ++++--- packages/vuetify/src/composables/list-items.ts | 4 +++- packages/vuetify/src/util/helpers.ts | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/vuetify/src/components/VCombobox/VCombobox.tsx b/packages/vuetify/src/components/VCombobox/VCombobox.tsx index 0dcec9ad76b..3dcbdc7bf68 100644 --- a/packages/vuetify/src/components/VCombobox/VCombobox.tsx +++ b/packages/vuetify/src/components/VCombobox/VCombobox.tsx @@ -133,13 +133,13 @@ export const VCombobox = genericComponent() const selectionIndex = shallowRef(-1) let cleared = false - const { items, transformIn, transformOut } = useItems(props) + const { items, transformIn, transformOut, emptyValues } = useItems(props) const { textColorClasses, textColorStyles } = useTextColor(() => vTextFieldRef.value?.color) const model = useProxiedModel( props, 'modelValue', [], - v => transformIn(wrapInArray(v)), + v => transformIn(v === null ? [null] : wrapInArray(v, emptyValues.value)), v => { const transformed = transformOut(v) return props.multiple ? transformed : (transformed[0] ?? null) @@ -478,7 +478,8 @@ export const VCombobox = genericComponent 0 + const isEmptyString = model.value.length === 1 && model.value[0].value === '' + const isDirty = model.value.length > 0 && !isEmptyString const textFieldProps = VTextField.filterProps(props) return ( diff --git a/packages/vuetify/src/composables/list-items.ts b/packages/vuetify/src/composables/list-items.ts index c93cc44ed4f..5ee1a3bf25c 100644 --- a/packages/vuetify/src/composables/list-items.ts +++ b/packages/vuetify/src/composables/list-items.ts @@ -123,6 +123,8 @@ export function transformItems ( export function useItems (props: ItemProps) { const items = computed(() => transformItems(props, props.items)) const hasNullItem = computed(() => items.value.some(item => item.value === null)) + const allValues = computed(() => items.value.map(item => item.value)) + const emptyValues = computed(() => ['', null, undefined].filter(v => !allValues.value.includes(v))) const itemsMap = shallowRef>(new Map()) const keylessItems = shallowRef([]) @@ -204,5 +206,5 @@ export function useItems (props: ItemProps) { : value.map(({ value }) => value) } - return { items, transformIn, transformOut } + return { items, transformIn, transformOut, emptyValues } } diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index 8662354797f..88cdeb4660d 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -389,11 +389,12 @@ export function arrayDiff (a: any[], b: any[]): any[] { type IfAny = 0 extends (1 & T) ? Y : N; export function wrapInArray ( - v: T | null | undefined + v: T | null | undefined, + emptyValues: any[] = [] ): T extends readonly any[] ? IfAny : NonNullable[] { - return v == null + return v == null || emptyValues.includes(v) ? [] as any : Array.isArray(v) ? v as any : [v] as any From 38cae099690f19fa8536ba9c71cb54af640cab18 Mon Sep 17 00:00:00 2001 From: John Chamver Puno Date: Sun, 14 Sep 2025 00:58:22 +0800 Subject: [PATCH 2/8] chore: add unit test for new functionality --- .../__tests__/VCombobox.spec.browser.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx index c00d34b9b1c..ee7f05d9c39 100644 --- a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx +++ b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx @@ -380,6 +380,22 @@ describe('VCombobox', () => { id: 'item2', }]) }) + + it('should show placeholder if initial value is empty string', async () => { + render(() => ( + + )) + + const inputField = screen.getByCSS('.v-field') + expect(inputField).toBeUndefined() + }) }) describe('readonly', () => { From e62bf0ba2c9b97cb1d8f29dc1334a1461472cc20 Mon Sep 17 00:00:00 2001 From: John Chamver Puno Date: Sun, 14 Sep 2025 02:11:05 +0800 Subject: [PATCH 3/8] fix: incorrect isDirty check and unit test --- .../vuetify/src/components/VCombobox/VCombobox.tsx | 3 +-- .../VCombobox/__tests__/VCombobox.spec.browser.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/vuetify/src/components/VCombobox/VCombobox.tsx b/packages/vuetify/src/components/VCombobox/VCombobox.tsx index 3dcbdc7bf68..fd8ea030460 100644 --- a/packages/vuetify/src/components/VCombobox/VCombobox.tsx +++ b/packages/vuetify/src/components/VCombobox/VCombobox.tsx @@ -478,8 +478,7 @@ export const VCombobox = genericComponent 0 && !isEmptyString + const isDirty = model.value.length > 0 const textFieldProps = VTextField.filterProps(props) return ( diff --git a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx index ee7f05d9c39..eebd60e730e 100644 --- a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx +++ b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx @@ -382,19 +382,22 @@ describe('VCombobox', () => { }) it('should show placeholder if initial value is empty string', async () => { - render(() => ( + const emptyString = ref('') + + const { getByPlaceholderText } = render(() => ( )) - const inputField = screen.getByCSS('.v-field') - expect(inputField).toBeUndefined() + const inputField = getByPlaceholderText('placeholder') + expect(inputField).toBeDisplayed() }) }) From fb9d9d4c933ff15090c0fdac39acb0fb23636ffe Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sat, 13 Sep 2025 23:04:50 +0200 Subject: [PATCH 4/8] chore: simplify test --- .../VCombobox/__tests__/VCombobox.spec.browser.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx index eebd60e730e..148f0a4b9f7 100644 --- a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx +++ b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx @@ -381,23 +381,18 @@ describe('VCombobox', () => { }]) }) - it('should show placeholder if initial value is empty string', async () => { + it('should show placeholder if initial value is empty string', () => { const emptyString = ref('') const { getByPlaceholderText } = render(() => ( )) - const inputField = getByPlaceholderText('placeholder') - expect(inputField).toBeDisplayed() + expect(getByPlaceholderText('select something')).toBeDisplayed() }) }) From d39fd228798a0a7673283d0e7ad6ee208a81c15c Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sat, 13 Sep 2025 23:20:42 +0200 Subject: [PATCH 5/8] chore: avoid random test failures (equivalent assertion) --- .../components/VCombobox/__tests__/VCombobox.spec.browser.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx index 148f0a4b9f7..97811c5de67 100644 --- a/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx +++ b/packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx @@ -392,7 +392,7 @@ describe('VCombobox', () => { /> )) - expect(getByPlaceholderText('select something')).toBeDisplayed() + expect(getByPlaceholderText('select something')).toBeVisible() }) }) From c5e4bccbfb5854344793ae97948a0774872bacbc Mon Sep 17 00:00:00 2001 From: Kael Date: Tue, 16 Sep 2025 16:57:27 +1000 Subject: [PATCH 6/8] perf: single loop --- packages/vuetify/src/composables/list-items.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/vuetify/src/composables/list-items.ts b/packages/vuetify/src/composables/list-items.ts index 5ee1a3bf25c..7084279f012 100644 --- a/packages/vuetify/src/composables/list-items.ts +++ b/packages/vuetify/src/composables/list-items.ts @@ -122,18 +122,25 @@ export function transformItems ( export function useItems (props: ItemProps) { const items = computed(() => transformItems(props, props.items)) - const hasNullItem = computed(() => items.value.some(item => item.value === null)) - const allValues = computed(() => items.value.map(item => item.value)) - const emptyValues = computed(() => ['', null, undefined].filter(v => !allValues.value.includes(v))) + const hasNullItem = shallowRef(false) + const emptyValues = shallowRef([]) const itemsMap = shallowRef>(new Map()) const keylessItems = shallowRef([]) watchEffect(() => { const _items = items.value + let _hasNullItem = false + const _emptyValues = new Set() const map = new Map() const keyless = [] for (let i = 0; i < _items.length; i++) { const item = _items[i] + if (item.value === null) { + _hasNullItem = true + } + if (item.value === '' || item.value == null) { + _emptyValues.add(item.value) + } if (isPrimitive(item.value) || item.value === null) { let values = map.get(item.value) if (!values) { @@ -145,6 +152,8 @@ export function useItems (props: ItemProps) { keyless.push(item) } } + hasNullItem.value = _hasNullItem + emptyValues.value = ['', null, undefined].filter(v => !_emptyValues.has(v)) itemsMap.value = map keylessItems.value = keyless }) From 19c15c24fae7dc723b9c6c6cbc831e6b24988405 Mon Sep 17 00:00:00 2001 From: John Chamver Puno Date: Wed, 24 Sep 2025 17:38:58 +0800 Subject: [PATCH 7/8] chore(VCombobox): simplify transformIn --- packages/vuetify/src/components/VCombobox/VCombobox.tsx | 2 +- packages/vuetify/src/util/helpers.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vuetify/src/components/VCombobox/VCombobox.tsx b/packages/vuetify/src/components/VCombobox/VCombobox.tsx index fd8ea030460..8a439953184 100644 --- a/packages/vuetify/src/components/VCombobox/VCombobox.tsx +++ b/packages/vuetify/src/components/VCombobox/VCombobox.tsx @@ -139,7 +139,7 @@ export const VCombobox = genericComponent transformIn(v === null ? [null] : wrapInArray(v, emptyValues.value)), + v => transformIn(wrapInArray(v, emptyValues)), v => { const transformed = transformOut(v) return props.multiple ? transformed : (transformed[0] ?? null) diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index 88cdeb4660d..2a16e625e9e 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -390,11 +390,11 @@ export function arrayDiff (a: any[], b: any[]): any[] { type IfAny = 0 extends (1 & T) ? Y : N; export function wrapInArray ( v: T | null | undefined, - emptyValues: any[] = [] + emptyValues: Ref ): T extends readonly any[] ? IfAny : NonNullable[] { - return v == null || emptyValues.includes(v) + return v == null || emptyValues?.value?.includes(v) ? [] as any : Array.isArray(v) ? v as any : [v] as any From 1d34431f69a60e1455d52827c42480061c2b463d Mon Sep 17 00:00:00 2001 From: John Chamver Puno Date: Wed, 24 Sep 2025 17:42:38 +0800 Subject: [PATCH 8/8] chore: make emptyValues parameter as optional --- packages/vuetify/src/util/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index 2a16e625e9e..7c1688a1148 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -390,7 +390,7 @@ export function arrayDiff (a: any[], b: any[]): any[] { type IfAny = 0 extends (1 & T) ? Y : N; export function wrapInArray ( v: T | null | undefined, - emptyValues: Ref + emptyValues?: Ref ): T extends readonly any[] ? IfAny : NonNullable[] {