diff --git a/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx b/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx index a66b7c02f9c..18f8919b28c 100644 --- a/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx +++ b/packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx @@ -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' @@ -73,7 +72,6 @@ export const makeVAutocompleteProps = propsFactory({ modelValue: null, role: 'combobox', }), ['validationValue', 'dirty', 'appendInnerIcon']), - ...makeTransitionProps({ transition: false }), }, 'VAutocomplete') type ItemType = T extends readonly (infer U)[] ? U : never @@ -126,6 +124,7 @@ export const VAutocomplete = genericComponent() const vVirtualScrollRef = ref() const selectionIndex = shallowRef(-1) + const _searchLock = shallowRef(null) const { items, transformIn, transformOut } = useItems(props) const { textColorClasses, textColorStyles } = useTextColor(() => vTextFieldRef.value?.color) const search = useProxiedModel(props, 'search', '') @@ -145,10 +144,13 @@ export const VAutocomplete = genericComponent 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 @@ -314,6 +316,7 @@ export const VAutocomplete = genericComponent { - 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) ) @@ -397,6 +404,7 @@ export const VAutocomplete = genericComponent= 0 && vVirtualScrollRef.value?.scrollToIndex(index) }) } + if (val) _searchLock.value = null }) watch(items, (newVal, oldVal) => { @@ -463,7 +471,6 @@ export const VAutocomplete = genericComponent { )) await userEvent.click(container) + await wait(100) // waitStable was very flaky here const menu = await screen.findByRole('listbox') @@ -128,6 +130,7 @@ describe('VAutocomplete', () => { )) await userEvent.click(container) + await commands.waitStable('.v-list') const menu = await screen.findByRole('listbox') @@ -179,6 +182,7 @@ describe('VAutocomplete', () => { )) await userEvent.click(container) + await wait(100) const menu = await screen.findByRole('listbox') @@ -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) @@ -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) @@ -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() }) @@ -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') @@ -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) @@ -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 diff --git a/packages/vuetify/src/components/VCombobox/VCombobox.tsx b/packages/vuetify/src/components/VCombobox/VCombobox.tsx index 2d2858d0160..4533be7be0b 100644 --- a/packages/vuetify/src/components/VCombobox/VCombobox.tsx +++ b/packages/vuetify/src/components/VCombobox/VCombobox.tsx @@ -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' @@ -79,7 +78,6 @@ export const makeVComboboxProps = propsFactory({ modelValue: null, role: 'combobox', }), ['validationValue', 'dirty', 'appendInnerIcon']), - ...makeTransitionProps({ transition: false }), }, 'VCombobox') type ItemType = T extends readonly (infer U)[] ? U : never @@ -151,6 +149,7 @@ export const VCombobox = genericComponent hasChips.value || !!slots.selection) const _search = shallowRef(!props.multiple && !hasSelectionSlot.value ? model.value[0]?.title ?? '' : '') + const _searchLock = shallowRef(null) const search = computed({ get: () => { @@ -193,11 +192,11 @@ export const VCombobox = genericComponent 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 @@ -296,16 +295,11 @@ export const VCombobox = genericComponent 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) { @@ -313,7 +307,7 @@ export const VCombobox = genericComponent { - menu.value = false + menu.value = keepMenu isPristine.value = true }) } @@ -446,8 +444,8 @@ export const VCombobox = genericComponent { - 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)) ) @@ -455,6 +453,8 @@ export const VCombobox = genericComponent= 0 && vVirtualScrollRef.value?.scrollToIndex(index) }) } + + if (val) _searchLock.value = null }) watch(items, (newVal, oldVal) => { @@ -520,7 +520,6 @@ export const VCombobox = genericComponent { )) await userEvent.click(element) + await wait(100) await userEvent.click((await screen.findAllByRole('option'))[0]) expect(model.value).toStrictEqual(items[0]) - expect(search.value).toBe(items[0].title) + await expect.poll(() => search.value).toBe(items[0].title) expect(screen.getByCSS('input')).toHaveValue(items[0].title) expect(screen.getByCSS('.v-combobox__selection')).toHaveTextContent(items[0].title) @@ -141,6 +143,7 @@ describe('VCombobox', () => { const input = screen.getByCSS('input') await userEvent.click(element) + await wait(100) await userEvent.click(screen.getAllByRole('option')[0]) expect(model.value).toStrictEqual([items[0]]) expect(search.value).toBeUndefined() @@ -366,6 +369,7 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await wait(100) const options = await screen.findAllByRole('option', { selected: true }) expect(options).toHaveLength(2) @@ -473,10 +477,11 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await wait(100) await userEvent.click(screen.getAllByRole('option')[0]) - expect(screen.getByCSS('input')).toHaveValue('0') + await expect.poll(() => screen.getByCSS('input')).toHaveValue('0') }) it('should conditionally show placeholder', async () => { @@ -544,6 +549,8 @@ describe('VCombobox', () => { expect(screen.queryAllByRole('listbox')).toHaveLength(0) await userEvent.click(element) + await commands.waitStable('.v-list') + expect(screen.queryAllByRole('listbox')).toHaveLength(1) await userEvent.keyboard('{Escape}') await expect.poll(() => screen.queryAllByRole('listbox')).toHaveLength(0) @@ -563,6 +570,8 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await commands.waitStable('.v-list') + expect(screen.getAllByRole('option')).toHaveLength(6) await userEvent.keyboard('Cal') @@ -585,6 +594,8 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await commands.waitStable('.v-list') + expect(screen.getAllByRole('option')).toHaveLength(6) await userEvent.keyboard('Cal') @@ -607,6 +618,8 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await commands.waitStable('.v-list') + expect(screen.getAllByRole('option')).toHaveLength(6) await userEvent.keyboard('Cal') @@ -668,6 +681,7 @@ describe('VCombobox', () => { }) await userEvent.click(element) + await commands.waitStable('.v-list') await expect(screen.findByRole('listbox')).resolves.toBeVisible() await userEvent.click(screen.getAllByRole('option')[0]) @@ -734,8 +748,9 @@ describe('VCombobox', () => { )) await userEvent.click(element) + await wait(100) await userEvent.click(screen.getAllByRole('option')[0]) - expect(model.value).toStrictEqual({ title: 'Item 1', value: 'item1' }) + await expect.poll(() => model.value).toStrictEqual({ title: 'Item 1', value: 'item1' }) await userEvent.click(document.body) expect(model.value).toStrictEqual({ title: 'Item 1', value: 'item1' })