Skip to content
Merged
23 changes: 17 additions & 6 deletions packages/vuetify/src/components/VCombobox/VCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const makeVComboboxProps = propsFactory({
delimiters: Array as PropType<readonly string[]>,

...makeFilterProps({ filterKeys: ['title'] }),
...makeSelectProps({ hideNoData: true, returnObject: true }),
...makeSelectProps({ hideNoData: false, returnObject: true }),
...omit(makeVTextFieldProps({
modelValue: null,
role: 'combobox',
Expand Down Expand Up @@ -127,6 +127,7 @@ export const VCombobox = genericComponent<new <
const isFocused = shallowRef(false)
const isPristine = shallowRef(true)
const listHasFocus = shallowRef(false)
const showAllItemsForNoMatch = shallowRef(false)
const vMenuRef = ref<VMenu>()
const vVirtualScrollRef = ref<VVirtualScroll>()
const selectionIndex = shallowRef(-1)
Expand Down Expand Up @@ -194,6 +195,9 @@ export const VCombobox = genericComponent<new <
if (props.hideSelected) {
return filteredItems.value.filter(filteredItem => !model.value.some(s => s.value === filteredItem.value))
}
if (filteredItems.value.length === 0 && showAllItemsForNoMatch.value) {
return items.value
}
return filteredItems.value
})

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

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

isPristine.value = !value
emit('update:search', value)
})

Expand Down Expand Up @@ -439,16 +445,22 @@ export const VCombobox = genericComponent<new <
}
})

watch(menu, () => {
if (!props.hideSelected && menu.value && model.value.length) {
watch(menu, val => {
if (!props.hideSelected && val && model.value.length) {
const index = displayItems.value.findIndex(
item => model.value.some(s => (props.valueComparator || deepEqual)(s.value, item.value))
)
IN_BROWSER && window.requestAnimationFrame(() => {
index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index)
})
}
})

if (val && search.value && filteredItems.value.length === 0) {
showAllItemsForNoMatch.value = true
}

isPristine.value = !search.value
}, { immediate: true })

watch(() => props.items, (newVal, oldVal) => {
if (menu.value) return
Expand Down Expand Up @@ -536,7 +548,6 @@ export const VCombobox = genericComponent<new <
{ !displayItems.value.length && !props.hideNoData && (slots['no-data']?.() ?? (
<VListItem key="no-data" title={ t(props.noDataText) } />
))}

<VVirtualScroll ref={ vVirtualScrollRef } renderless items={ displayItems.value } itemKey="value">
{ ({ item, index, itemRef }) => {
const itemProps = mergeProps(item.props, {
Expand Down Expand Up @@ -692,7 +703,7 @@ export const VCombobox = genericComponent<new <
'append-inner': (...args) => (
<>
{ slots['append-inner']?.(...args) }
{ (!props.hideNoData || props.items.length) && props.menuIcon ? (
{ props.menuIcon ? (
<VIcon
class="v-combobox__menu-icon"
color={ vTextFieldRef.value?.fieldIconColor }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,41 @@ describe('VCombobox', () => {
expect(model.value).toEqual(expected)
})

it('should show only matching items when reopening the menu', async () => {
const { element } = render(() => (
<VCombobox items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']} />
))

await userEvent.click(element)
await userEvent.keyboard('c')
await expect(screen.findAllByRole('option')).resolves.toHaveLength(2)
await userEvent.keyboard('al')
await expect(screen.findAllByRole('option')).resolves.toHaveLength(1)
await userEvent.click(document.body)
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)
await userEvent.click(element)
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(1)
})

it('should show only matching items when opening for the first time', async () => {
const model = ref('flor')
const { element } = render(() => (
<VCombobox
v-model={ model.value }
items={['California', 'Colorado', 'Florida', 'Georgia', 'Texas', 'Wyoming']}
/>
))

await userEvent.click(element)
expect(screen.getAllByRole('option')).toHaveLength(1)
await userEvent.click(document.body)
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(0)

// expect same behavior when re-opening the menu
await userEvent.click(element)
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(1)
})

describe('Showcase', () => {
generate({ stories })
})
Expand Down