Skip to content

Commit 3983af9

Browse files
committed
fix(VCombobox): unstable menu state while typing (#22045)
1 parent 13a37f4 commit 3983af9

File tree

2 files changed

+41
-7
lines changed

2 files changed

+41
-7
lines changed

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,13 @@ export const VCombobox = genericComponent<new <
190190
: (props.multiple ? model.value.length : search.value.length)
191191
})
192192

193-
const { filteredItems, getMatches } = useFilter(props, items, () => isPristine.value ? '' : search.value)
193+
const { filteredItems, getMatches } = useFilter(props, items, search)
194+
195+
const hasMatchingItems = computed(() => {
196+
return props.hideSelected
197+
? filteredItems.value.some(filteredItem => !model.value.some(s => s.value === filteredItem.value))
198+
: filteredItems.value.length > 0
199+
})
194200

195201
const displayItems = computed(() => {
196202
if (props.hideSelected) {
@@ -202,10 +208,9 @@ export const VCombobox = genericComponent<new <
202208
return filteredItems.value
203209
})
204210

205-
const menuDisabled = computed(() => (
206-
(props.hideNoData && !displayItems.value.length) ||
207-
form.isReadonly.value || form.isDisabled.value
208-
))
211+
const menuDisabled = computed(() => {
212+
return form.isReadonly.value || form.isDisabled.value
213+
})
209214
const _menu = useProxiedModel(props, 'menu')
210215
const menu = computed({
211216
get: () => _menu.value,
@@ -225,7 +230,9 @@ export const VCombobox = genericComponent<new <
225230
// then search computed triggers and updates _search to ''
226231
nextTick(() => (cleared = false))
227232
} else if (isFocused.value && !menu.value) {
228-
menu.value = true
233+
menu.value = hasMatchingItems.value || !props.hideNoData
234+
} else if (isFocused.value && menu.value && !hasMatchingItems.value && props.hideNoData) {
235+
menu.value = false
229236
}
230237

231238
isPristine.value = !value
@@ -456,7 +463,7 @@ export const VCombobox = genericComponent<new <
456463
})
457464
}
458465

459-
if (val && search.value && filteredItems.value.length === 0) {
466+
if (val && search.value && !hasMatchingItems.value) {
460467
showAllItemsForNoMatch.value = true
461468
}
462469

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,33 @@ describe('VCombobox', () => {
238238
await userEvent.keyboard('antonsen')
239239
await expect(screen.findByRole('option')).resolves.toHaveTextContent('Antonsen PK')
240240
})
241+
242+
// https://github.com/vuetifyjs/vuetify/pull/22045
243+
it('should keep menu open while typing', async () => {
244+
const items = ['Item 1', 'Item 1a', 'Another']
245+
246+
const { element } = render(() => (
247+
<VCombobox items={ items } multiple />
248+
))
249+
250+
const optionsPerAction = [
251+
['1', 2],
252+
['a', 1],
253+
['x', 0],
254+
['y', 0],
255+
['{Backspace}', 0],
256+
['{Backspace}', 1],
257+
['{Backspace}', 2],
258+
['{Backspace}', 3],
259+
] as const
260+
261+
await userEvent.click(element)
262+
await expect(screen.findAllByRole('option')).resolves.toHaveLength(3)
263+
for (const [text, expectedItemsCount] of optionsPerAction) {
264+
await userEvent.keyboard(text)
265+
await expect.poll(() => screen.queryAllByRole('option')).toHaveLength(expectedItemsCount)
266+
}
267+
})
241268
})
242269

243270
describe('prefilled data', () => {

0 commit comments

Comments
 (0)