Skip to content

Commit eeb9d14

Browse files
jcjpJ-Sek
andauthored
fix(VCombobox): filter matching items when opening first time (#21901)
Co-authored-by: J-Sek <[email protected]>
1 parent 9ec5a0d commit eeb9d14

File tree

2 files changed

+52
-6
lines changed

2 files changed

+52
-6
lines changed

packages/vuetify/src/components/VCombobox/VCombobox.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const makeVComboboxProps = propsFactory({
7272
delimiters: Array as PropType<readonly string[]>,
7373

7474
...makeFilterProps({ filterKeys: ['title'] }),
75-
...makeSelectProps({ hideNoData: true, returnObject: true }),
75+
...makeSelectProps({ hideNoData: false, returnObject: true }),
7676
...omit(makeVTextFieldProps({
7777
modelValue: null,
7878
role: 'combobox',
@@ -127,6 +127,7 @@ export const VCombobox = genericComponent<new <
127127
const isFocused = shallowRef(false)
128128
const isPristine = shallowRef(true)
129129
const listHasFocus = shallowRef(false)
130+
const showAllItemsForNoMatch = shallowRef(false)
130131
const vMenuRef = ref<VMenu>()
131132
const vVirtualScrollRef = ref<VVirtualScroll>()
132133
const selectionIndex = shallowRef(-1)
@@ -194,6 +195,9 @@ export const VCombobox = genericComponent<new <
194195
if (props.hideSelected) {
195196
return filteredItems.value.filter(filteredItem => !model.value.some(s => s.value === filteredItem.value))
196197
}
198+
if (filteredItems.value.length === 0 && showAllItemsForNoMatch.value) {
199+
return items.value
200+
}
197201
return filteredItems.value
198202
})
199203

@@ -214,6 +218,7 @@ export const VCombobox = genericComponent<new <
214218
const label = toRef(() => menu.value ? props.closeText : props.openText)
215219

216220
watch(_search, value => {
221+
showAllItemsForNoMatch.value = false
217222
if (cleared) {
218223
// wait for clear to finish, VTextField sets _search to null
219224
// then search computed triggers and updates _search to ''
@@ -222,6 +227,7 @@ export const VCombobox = genericComponent<new <
222227
menu.value = true
223228
}
224229

230+
isPristine.value = !value
225231
emit('update:search', value)
226232
})
227233

@@ -439,16 +445,22 @@ export const VCombobox = genericComponent<new <
439445
}
440446
})
441447

442-
watch(menu, () => {
443-
if (!props.hideSelected && menu.value && model.value.length) {
448+
watch(menu, val => {
449+
if (!props.hideSelected && val && model.value.length) {
444450
const index = displayItems.value.findIndex(
445451
item => model.value.some(s => (props.valueComparator || deepEqual)(s.value, item.value))
446452
)
447453
IN_BROWSER && window.requestAnimationFrame(() => {
448454
index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index)
449455
})
450456
}
451-
})
457+
458+
if (val && search.value && filteredItems.value.length === 0) {
459+
showAllItemsForNoMatch.value = true
460+
}
461+
462+
isPristine.value = !search.value
463+
}, { immediate: true })
452464

453465
watch(() => props.items, (newVal, oldVal) => {
454466
if (menu.value) return
@@ -536,7 +548,6 @@ export const VCombobox = genericComponent<new <
536548
{ !displayItems.value.length && !props.hideNoData && (slots['no-data']?.() ?? (
537549
<VListItem key="no-data" title={ t(props.noDataText) } />
538550
))}
539-
540551
<VVirtualScroll ref={ vVirtualScrollRef } renderless items={ displayItems.value } itemKey="value">
541552
{ ({ item, index, itemRef }) => {
542553
const itemProps = mergeProps(item.props, {
@@ -692,7 +703,7 @@ export const VCombobox = genericComponent<new <
692703
'append-inner': (...args) => (
693704
<>
694705
{ slots['append-inner']?.(...args) }
695-
{ (!props.hideNoData || props.items.length) && props.menuIcon ? (
706+
{ props.menuIcon ? (
696707
<VIcon
697708
class="v-combobox__menu-icon"
698709
color={ vTextFieldRef.value?.fieldIconColor }

packages/vuetify/src/components/VCombobox/__tests__/VCombobox.spec.browser.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,41 @@ describe('VCombobox', () => {
771771
expect(model.value).toEqual(expected)
772772
})
773773

774+
it('should show only matching items when reopening the menu', async () => {
775+
const { element } = render(() => (
776+
<VCombobox items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']} />
777+
))
778+
779+
await userEvent.click(element)
780+
await userEvent.keyboard('c')
781+
await expect(screen.findAllByRole('option')).resolves.toHaveLength(2)
782+
await userEvent.keyboard('al')
783+
await expect(screen.findAllByRole('option')).resolves.toHaveLength(1)
784+
await userEvent.click(document.body)
785+
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
786+
await userEvent.click(element)
787+
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(1)
788+
})
789+
790+
it('should show only matching items when opening for the first time', async () => {
791+
const model = ref('flor')
792+
const { element } = render(() => (
793+
<VCombobox
794+
v-model={ model.value }
795+
items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
796+
/>
797+
))
798+
799+
await userEvent.click(element)
800+
expect(screen.getAllByRole('option')).toHaveLength(1)
801+
await userEvent.click(document.body)
802+
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
803+
804+
// expect same behavior when re-opening the menu
805+
await userEvent.click(element)
806+
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(1)
807+
})
808+
774809
describe('Showcase', () => {
775810
generate({ stories })
776811
})

0 commit comments

Comments
 (0)