Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { useMenuActivator } from '@/composables/menuActivator'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

// Utilities
import { computed, mergeProps, nextTick, ref, shallowRef, watch } from 'vue'
Expand Down Expand Up @@ -73,7 +72,6 @@ export const makeVAutocompleteProps = propsFactory({
modelValue: null,
role: 'combobox',
}), ['validationValue', 'dirty', 'appendInnerIcon']),
...makeTransitionProps({ transition: false }),
}, 'VAutocomplete')

type ItemType<T> = T extends readonly (infer U)[] ? U : never
Expand Down Expand Up @@ -126,6 +124,7 @@ export const VAutocomplete = genericComponent<new <
const vMenuRef = ref<VMenu>()
const vVirtualScrollRef = ref<VVirtualScroll>()
const selectionIndex = shallowRef(-1)
const _searchLock = shallowRef<string | null>(null)
const { items, transformIn, transformOut } = useItems(props)
const { textColorClasses, textColorStyles } = useTextColor(() => vTextFieldRef.value?.color)
const search = useProxiedModel(props, 'search', '')
Expand All @@ -145,10 +144,13 @@ export const VAutocomplete = genericComponent<new <
: model.value.length
})
const form = useForm(props)
const { filteredItems, getMatches } = useFilter(props, items, () => isPristine.value ? '' : search.value)
const { filteredItems, getMatches } = useFilter(
props,
items,
() => _searchLock.value ?? (isPristine.value ? '' : search.value))

const displayItems = computed(() => {
if (props.hideSelected) {
if (props.hideSelected && _searchLock.value === null) {
return filteredItems.value.filter(filteredItem => !model.value.some(s => s.value === filteredItem.value))
}
return filteredItems.value
Expand Down Expand Up @@ -314,6 +316,7 @@ export const VAutocomplete = genericComponent<new <
isPristine.value = true
vTextFieldRef.value?.focus()
}
_searchLock.value = null
}

function onFocusin (e: FocusEvent) {
Expand Down Expand Up @@ -353,6 +356,7 @@ export const VAutocomplete = genericComponent<new <
} else {
const add = set !== false
model.value = add ? [item] : []
_searchLock.value = isPristine.value ? '' : (search.value ?? '')
search.value = add && !hasSelectionSlot.value ? item.title : ''

// watch for search watcher to trigger
Expand All @@ -375,6 +379,9 @@ export const VAutocomplete = genericComponent<new <
} else {
if (!props.multiple && search.value == null) model.value = []
menu.value = false
if (!isPristine.value && search.value) {
_searchLock.value = search.value
}
search.value = ''
selectionIndex.value = -1
}
Expand All @@ -388,15 +395,16 @@ export const VAutocomplete = genericComponent<new <
isPristine.value = !val
})

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

watch(items, (newVal, oldVal) => {
Expand Down Expand Up @@ -463,7 +471,6 @@ export const VAutocomplete = genericComponent<new <
maxHeight={ 310 }
openOnClick={ false }
closeOnContentClick={ false }
transition={ props.transition }
onAfterEnter={ onAfterEnter }
onAfterLeave={ onAfterLeave }
{ ...props.menuProps }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { VAutocomplete } from '../VAutocomplete'
import { VForm } from '@/components/VForm'

// Utilities
import { generate, render, screen, userEvent, waitAnimationFrame, waitIdle } from '@test'
import { generate, render, screen, userEvent, wait, waitAnimationFrame, waitIdle } from '@test'
import { findAllByRole, queryAllByRole, within } from '@testing-library/vue'
import { commands } from '@vitest/browser/context'
import { cloneVNode, ref } from 'vue'

const variants = ['underlined', 'outlined', 'filled', 'solo', 'plain'] as const
Expand Down Expand Up @@ -82,6 +83,7 @@ describe('VAutocomplete', () => {
))

await userEvent.click(container)
await wait(100) // waitStable was very flaky here

const menu = await screen.findByRole('listbox')

Expand Down Expand Up @@ -128,6 +130,7 @@ describe('VAutocomplete', () => {
))

await userEvent.click(container)
await commands.waitStable('.v-list')

const menu = await screen.findByRole('listbox')

Expand Down Expand Up @@ -179,6 +182,7 @@ describe('VAutocomplete', () => {
))

await userEvent.click(container)
await wait(100)

const menu = await screen.findByRole('listbox')

Expand Down Expand Up @@ -354,6 +358,7 @@ describe('VAutocomplete', () => {
))

await userEvent.click(element)
await commands.waitStable('.v-list')

await userEvent.click(screen.getAllByRole('option')[0])
expect(selectedItems.value).toBe(1)
Expand Down Expand Up @@ -422,6 +427,7 @@ describe('VAutocomplete', () => {

const menuIcon = screen.getByRole('button', { name: /open/i })
await userEvent.click(menuIcon)
await commands.waitStable('.v-list')

const listItems = screen.getAllByRole('option')
expect(listItems).toHaveLength(2)
Expand All @@ -444,8 +450,7 @@ describe('VAutocomplete', () => {
))

await userEvent.type(element, 'f')
const listItems = screen.getAllByRole('option')
expect(listItems).toHaveLength(2)
await expect.poll(() => screen.findAllByRole('option')).toHaveLength(2)
expect(selectedItems.value).toBeUndefined()
})

Expand Down Expand Up @@ -535,6 +540,7 @@ describe('VAutocomplete', () => {
const getItems = () => screen.queryAllByRole('option')

await userEvent.click(element)
await commands.waitStable('.v-list')
await expect.poll(getItems).toHaveLength(6)

await userEvent.keyboard('Cal')
Expand Down Expand Up @@ -615,7 +621,7 @@ describe('VAutocomplete', () => {
))

await userEvent.click(element)

await commands.waitStable('.v-list')
const items = await screen.findAllByRole('option')
expect(items).toHaveLength(2)

Expand All @@ -633,7 +639,7 @@ describe('VAutocomplete', () => {
expect(screen.queryByRole('listbox')).toBeNull()

await rerender({ items: ['Foo', 'Bar'] })
await expect(screen.findByRole('listbox')).resolves.toBeDisplayed()
await expect(screen.findByRole('listbox')).resolves.toBeVisible()
})

// https://github.com/vuetifyjs/vuetify/issues/19346
Expand Down
39 changes: 19 additions & 20 deletions packages/vuetify/src/components/VCombobox/VCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { transformItem, useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { useMenuActivator } from '@/composables/menuActivator'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

// Utilities
import { computed, mergeProps, nextTick, ref, shallowRef, watch } from 'vue'
Expand Down Expand Up @@ -79,7 +78,6 @@ export const makeVComboboxProps = propsFactory({
modelValue: null,
role: 'combobox',
}), ['validationValue', 'dirty', 'appendInnerIcon']),
...makeTransitionProps({ transition: false }),
}, 'VCombobox')

type ItemType<T> = T extends readonly (infer U)[] ? U : never
Expand Down Expand Up @@ -151,6 +149,7 @@ export const VCombobox = genericComponent<new <
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection)

const _search = shallowRef(!props.multiple && !hasSelectionSlot.value ? model.value[0]?.title ?? '' : '')
const _searchLock = shallowRef<string | null>(null)

const search = computed<string>({
get: () => {
Expand Down Expand Up @@ -193,11 +192,11 @@ export const VCombobox = genericComponent<new <
const { filteredItems, getMatches } = useFilter(
props,
items,
() => props.alwaysFilter || !isPristine.value ? search.value : ''
() => _searchLock.value ?? (props.alwaysFilter || !isPristine.value ? search.value : '')
)

const displayItems = computed(() => {
if (props.hideSelected) {
if (props.hideSelected && _searchLock.value === null) {
return filteredItems.value.filter(filteredItem => !model.value.some(s => s.value === filteredItem.value))
}
return filteredItems.value
Expand Down Expand Up @@ -296,24 +295,19 @@ export const VCombobox = genericComponent<new <
menu.value = false
}

if (['Enter', 'Escape', 'Tab'].includes(e.key)) {
if (
highlightFirst.value &&
['Enter', 'Tab'].includes(e.key) &&
!model.value.some(({ value }) => value === displayItems.value[0].value)
) {
select(filteredItems.value[0])
}

isPristine.value = true
if (highlightFirst.value &&
['Enter', 'Tab'].includes(e.key) &&
!model.value.some(({ value }) => value === displayItems.value[0].value)
) {
select(filteredItems.value[0])
}

if (e.key === 'ArrowDown' && highlightFirst.value) {
listRef.value?.focus('next')
}

if (e.key === 'Enter' && search.value) {
select(transformItem(props, search.value))
select(transformItem(props, search.value), true, true)
if (hasSelectionSlot.value) _search.value = ''
}

Expand Down Expand Up @@ -378,9 +372,10 @@ export const VCombobox = genericComponent<new <
isPristine.value = true
vTextFieldRef.value?.focus()
}
_searchLock.value = null
}
/** @param set - null means toggle */
function select (item: ListItem | undefined, set: boolean | null = true) {
function select (item: ListItem | undefined, set: boolean | null = true, keepMenu = false) {
if (!item || item.props.disabled) return

if (props.multiple) {
Expand All @@ -401,11 +396,14 @@ export const VCombobox = genericComponent<new <
} else {
const add = set !== false
model.value = add ? [item] : []
if ((!isPristine.value || props.alwaysFilter) && _search.value) {
_searchLock.value = _search.value
}
_search.value = add && !hasSelectionSlot.value ? item.title : ''

// watch for search watcher to trigger
nextTick(() => {
menu.value = false
menu.value = keepMenu
isPristine.value = true
})
}
Expand Down Expand Up @@ -446,15 +444,17 @@ 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 && isPristine.value) {
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) _searchLock.value = null
})

watch(items, (newVal, oldVal) => {
Expand Down Expand Up @@ -520,7 +520,6 @@ export const VCombobox = genericComponent<new <
maxHeight={ 310 }
openOnClick={ false }
closeOnContentClick={ false }
transition={ props.transition }
onAfterEnter={ onAfterEnter }
onAfterLeave={ onAfterLeave }
{ ...props.menuProps }
Expand Down
Loading