Skip to content

Commit

Permalink
fix(VAutocomplete/VCombobox): position w/ selection slot (#19294)
Browse files Browse the repository at this point in the history
fixes #17573

Co-authored-by: John Leider <john@vuetifyjs.com>
  • Loading branch information
yuwu9145 and johnleider authored Mar 1, 2024
1 parent 86d0c32 commit 1e57453
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
.v-field__input > input
caret-color: transparent

&--single
&--single:not(.v-autocomplete--selection-slot)
&.v-text-field .v-field--focused input
flex: 1 1
position: absolute
Expand Down
16 changes: 9 additions & 7 deletions packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export const VAutocomplete = genericComponent<new <
return filteredItems.value
})

const hasChips = computed(() => !!(props.chips || slots.chip))
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection)

const selectedValues = computed(() => model.value.map(selection => selection.props.value))

const highlightFirst = computed(() => {
Expand Down Expand Up @@ -343,7 +346,7 @@ export const VAutocomplete = genericComponent<new <
} else {
const add = set !== false
model.value = add ? [item] : []
search.value = add ? item.title : ''
search.value = add && !hasSelectionSlot.value ? item.title : ''

// watch for search watcher to trigger
nextTick(() => {
Expand All @@ -358,7 +361,7 @@ export const VAutocomplete = genericComponent<new <

if (val) {
isSelecting.value = true
search.value = props.multiple ? '' : String(model.value.at(-1)?.props.title ?? '')
search.value = (props.multiple || hasSelectionSlot.value) ? '' : String(model.value.at(-1)?.props.title ?? '')
isPristine.value = true

nextTick(() => isSelecting.value = false)
Expand Down Expand Up @@ -403,7 +406,6 @@ export const VAutocomplete = genericComponent<new <
})

useRender(() => {
const hasChips = !!(props.chips || slots.chip)
const hasList = !!(
(!props.hideNoData || displayItems.value.length) ||
slots['prepend-item'] ||
Expand All @@ -430,7 +432,7 @@ export const VAutocomplete = genericComponent<new <
{
'v-autocomplete--active-menu': menu.value,
'v-autocomplete--chips': !!props.chips,
'v-autocomplete--selection-slot': !!slots.selection,
'v-autocomplete--selection-slot': !!hasSelectionSlot.value,
'v-autocomplete--selecting-index': selectionIndex.value > -1,
},
props.class,
Expand Down Expand Up @@ -551,10 +553,10 @@ export const VAutocomplete = genericComponent<new <
'onUpdate:modelValue': undefined,
}

const hasSlot = hasChips ? !!slots.chip : !!slots.selection
const hasSlot = hasChips.value ? !!slots.chip : !!slots.selection
const slotContent = hasSlot
? ensureValidVNode(
hasChips
hasChips.value
? slots.chip!({ item, index, props: slotProps })
: slots.selection!({ item, index })
)
Expand All @@ -574,7 +576,7 @@ export const VAutocomplete = genericComponent<new <
]}
style={ index === selectionIndex.value ? textColorStyles.value : {} }
>
{ hasChips ? (
{ hasChips.value ? (
!slots.chip ? (
<VChip
key="chip"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,39 @@ describe('VAutocomplete', () => {
.should('exist')
})

// https://github.com/vuetifyjs/vuetify/issues/17573
// When using selection slot or chips, input displayed next to chip/selection slot should be always empty
it('should always have empty input value when it is unfocused and when using selection slot or chips', () => {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
const selectedItem = ref('Item 1')

cy
.mount(() => (
<VAutocomplete
items={ items }
chips
v-model={ selectedItem.value }
/>
))
.get('.v-autocomplete').click()
.get('.v-autocomplete input').should('have.value', '')
// Blur input with a custom search input value
.type('test')
.blur()
.should('have.value', '')
.should(() => {
expect(selectedItem.value).to.equal('Item 1')
})
// Search existing item and click to select
.get('.v-autocomplete').click()
.get('.v-autocomplete input').should('have.value', '')
.type('Item 1')
.get('.v-list-item').eq(0).click({ waitForAnimations: false })
.should(() => {
expect(selectedItem.value).to.equal('Item 1')
})
})

describe('Showcase', () => {
generate({ stories })
})
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/components/VCombobox/VCombobox.sass
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
.v-field__input > input
caret-color: transparent

&--single
&--single:not(.v-combobox--selection-slot)
&.v-text-field .v-field--focused input
flex: 1 1
position: absolute
Expand Down
35 changes: 20 additions & 15 deletions packages/vuetify/src/components/VCombobox/VCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,19 @@ export const VCombobox = genericComponent<new <
}
)
const form = useForm()
const _search = shallowRef(!props.multiple ? model.value[0]?.title ?? '' : '')

const hasChips = computed(() => !!(props.chips || slots.chip))
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection)

const _search = shallowRef(!props.multiple && !hasSelectionSlot.value ? model.value[0]?.title ?? '' : '')

const search = computed<string>({
get: () => {
return _search.value
},
set: (val: string | null) => {
_search.value = val ?? ''
if (!props.multiple) {
if (!props.multiple && !hasSelectionSlot.value) {
model.value = [transformItem(props, val)]
}

Expand Down Expand Up @@ -208,8 +213,9 @@ export const VCombobox = genericComponent<new <

emit('update:search', value)
})

watch(model, value => {
if (!props.multiple) {
if (!props.multiple && !hasSelectionSlot.value) {
_search.value = value[0]?.title ?? ''
}
})
Expand Down Expand Up @@ -295,6 +301,11 @@ export const VCombobox = genericComponent<new <
listRef.value?.focus('next')
}

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

if (!props.multiple) return

if (['Backspace', 'Delete'].includes(e.key)) {
Expand Down Expand Up @@ -340,11 +351,6 @@ export const VCombobox = genericComponent<new <
vTextFieldRef.value.setSelectionRange(0, 0)
}
}

if (e.key === 'Enter' && search.value) {
select(transformItem(props, search.value))
search.value = ''
}
}
function onAfterLeave () {
if (isFocused.value) {
Expand Down Expand Up @@ -374,7 +380,7 @@ export const VCombobox = genericComponent<new <
} else {
const add = set !== false
model.value = add ? [item] : []
_search.value = add ? item.title : ''
_search.value = add && !hasSelectionSlot.value ? item.title : ''

// watch for search watcher to trigger
nextTick(() => {
Expand Down Expand Up @@ -409,7 +415,7 @@ export const VCombobox = genericComponent<new <
!model.value.some(({ value }) => value === displayItems.value[0].value)
) {
select(displayItems.value[0])
} else if (props.multiple && search.value) {
} else if (search.value) {
select(transformItem(props, search.value))
}
})
Expand All @@ -432,7 +438,6 @@ export const VCombobox = genericComponent<new <
})

useRender(() => {
const hasChips = !!(props.chips || slots.chip)
const hasList = !!(
(!props.hideNoData || displayItems.value.length) ||
slots['prepend-item'] ||
Expand All @@ -457,7 +462,7 @@ export const VCombobox = genericComponent<new <
{
'v-combobox--active-menu': menu.value,
'v-combobox--chips': !!props.chips,
'v-combobox--selection-slot': !!slots.selection,
'v-combobox--selection-slot': !!hasSelectionSlot.value,
'v-combobox--selecting-index': selectionIndex.value > -1,
[`v-combobox--${props.multiple ? 'multiple' : 'single'}`]: true,
},
Expand Down Expand Up @@ -579,10 +584,10 @@ export const VCombobox = genericComponent<new <
'onUpdate:modelValue': undefined,
}

const hasSlot = hasChips ? !!slots.chip : !!slots.selection
const hasSlot = hasChips.value ? !!slots.chip : !!slots.selection
const slotContent = hasSlot
? ensureValidVNode(
hasChips
hasChips.value
? slots.chip!({ item, index, props: slotProps })
: slots.selection!({ item, index })
)
Expand All @@ -602,7 +607,7 @@ export const VCombobox = genericComponent<new <
]}
style={ index === selectionIndex.value ? textColorStyles.value : {} }
>
{ hasChips ? (
{ hasChips.value ? (
!slots.chip ? (
<VChip
key="chip"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,48 @@ describe('VCombobox', () => {
.should('exist')
})

// https://github.com/vuetifyjs/vuetify/issues/17573
// When using selection slot or chips, input displayed next to chip/selection slot should be always empty
it('should always have empty input value when it is unfocused and when using selection slot or chips', () => {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
const selectedItem = ref('Item 1')

cy
.mount(() => (
<VCombobox
items={ items }
chips
v-model={ selectedItem.value }
/>
))
.get('.v-combobox').click()
.get('.v-combobox input').should('have.value', '')
// Blur input with a custom search input value
.type('test')
.blur()
.should('have.value', '')
.should(() => {
expect(selectedItem.value).to.equal('test')
})
// Press enter key with a custom search input value
.get('.v-combobox').click()
.get('.v-combobox input').should('have.value', '')
.type('test 2')
.trigger('keydown', { key: keyValues.enter, waitForAnimations: false })
.should('have.value', '')
.should(() => {
expect(selectedItem.value).to.equal('test 2')
})
// Search existing item and click to select
.get('.v-combobox').click()
.get('.v-combobox input').type('Item 1')
.get('.v-list-item').eq(0).click({ waitForAnimations: false })
.get('.v-combobox input').should('have.value', '')
.should(() => {
expect(selectedItem.value).to.equal('Item 1')
})
})

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

0 comments on commit 1e57453

Please sign in to comment.