Skip to content

Commit

Permalink
fix(VSelect/VAutocomplete/VCombobox): respect click outside (vuetifyj…
Browse files Browse the repository at this point in the history
…s#20004)

fixes vuetifyjs#20003

Co-authored-by: John Leider <[email protected]>
  • Loading branch information
2 people authored and KaelWD committed Jan 30, 2025
1 parent 4c889f7 commit e85225d
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 3 deletions.
10 changes: 9 additions & 1 deletion packages/vuetify/src/components/VAutocomplete/VAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useForm } from '@/composables/form'
import { forwardRefs } from '@/composables/forwardRefs'
import { useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { useIsMousedown } from '@/composables/mousedown'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

Expand Down Expand Up @@ -150,6 +151,7 @@ export const VAutocomplete = genericComponent<new <
const label = computed(() => menu.value ? props.closeText : props.openText)
const { items, transformIn, transformOut } = useItems(props)
const { textColorClasses, textColorStyles } = useTextColor(color)
const { isMousedown } = useIsMousedown()
const search = useProxiedModel(props, 'search', '')
const model = useProxiedModel(
props,
Expand Down Expand Up @@ -373,6 +375,12 @@ export const VAutocomplete = genericComponent<new <
}
}

function onBlur (e: FocusEvent) {
if (!isMousedown.value) {
menu.value = false
}
}

watch(isFocused, (val, oldVal) => {
if (val === oldVal) return

Expand All @@ -384,7 +392,6 @@ export const VAutocomplete = genericComponent<new <
nextTick(() => isSelecting.value = false)
} else {
if (!props.multiple && search.value == null) model.value = []
menu.value = false
if (!model.value.some(({ title }) => title === search.value)) search.value = ''
selectionIndex.value = -1
}
Expand Down Expand Up @@ -453,6 +460,7 @@ export const VAutocomplete = genericComponent<new <
readonly={ form.isReadonly.value }
placeholder={ isDirty ? undefined : props.placeholder }
onClick:clear={ onClear }
onBlur={ onBlur }
onMousedown:control={ onMousedownControl }
onKeydown={ onKeydown }
>
Expand Down
11 changes: 10 additions & 1 deletion packages/vuetify/src/components/VCombobox/VCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useForm } from '@/composables/form'
import { forwardRefs } from '@/composables/forwardRefs'
import { transformItem, useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { useIsMousedown } from '@/composables/mousedown'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

Expand Down Expand Up @@ -166,6 +167,7 @@ export const VCombobox = genericComponent<new <
}
)
const form = useForm(props)
const { isMousedown } = useIsMousedown()

const hasChips = computed(() => !!(props.chips || slots.chip))
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection)
Expand Down Expand Up @@ -376,6 +378,13 @@ export const VCombobox = genericComponent<new <
vTextFieldRef.value?.focus()
}
}

function onBlur (e: FocusEvent) {
if (!isMousedown.value) {
menu.value = false
}
}

/** @param set - null means toggle */
function select (item: ListItem | undefined, set: boolean | null = true) {
if (!item || item.props.disabled) return
Expand Down Expand Up @@ -425,7 +434,6 @@ export const VCombobox = genericComponent<new <
if (val || val === oldVal) return

selectionIndex.value = -1
menu.value = false

if (search.value) {
if (props.multiple) {
Expand Down Expand Up @@ -497,6 +505,7 @@ export const VCombobox = genericComponent<new <
readonly={ form.isReadonly.value }
placeholder={ isDirty ? undefined : props.placeholder }
onClick:clear={ onClear }
onBlur={ onBlur }
onMousedown:control={ onMousedownControl }
onKeydown={ onKeydown }
>
Expand Down
4 changes: 3 additions & 1 deletion packages/vuetify/src/components/VSelect/VSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { forwardRefs } from '@/composables/forwardRefs'
import { IconValue } from '@/composables/icons'
import { makeItemsProps, useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { useIsMousedown } from '@/composables/mousedown'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

Expand Down Expand Up @@ -168,6 +169,7 @@ export const VSelect = genericComponent<new <
: model.value.length
})
const form = useForm(props)
const { isMousedown } = useIsMousedown()
const selectedValues = computed(() => model.value.map(selection => selection.value))
const isFocused = shallowRef(false)
const label = computed(() => menu.value ? props.closeText : props.openText)
Expand Down Expand Up @@ -282,7 +284,7 @@ export const VSelect = genericComponent<new <
}
}
function onBlur (e: FocusEvent) {
if (!listRef.value?.$el.contains(e.relatedTarget as HTMLElement)) {
if (!listRef.value?.$el.contains(e.relatedTarget as HTMLElement) && !isMousedown.value) {
menu.value = false
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/vuetify/src/composables/mousedown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// https://github.com/vuetifyjs/vuetify/issues/20003
/**
* This composable is designed to track whether the mouse is in a mousedown state at any given time. The original motivation is that
* it is impossible to distinguish whether a blur event is triggered by mousedown, keydown, or via JavaScript.
* This composable allows for conditional logic when a blur is triggered by mousedown.
*/

// Utilities
import { onMounted, onUnmounted, shallowRef } from 'vue'

export function useIsMousedown () {
const isMousedown = shallowRef(false)

function mousedown () {
isMousedown.value = true
}

function mouseup () {
isMousedown.value = false
}

onMounted(() => {
document.body.addEventListener('mousedown', mousedown)
document.body.addEventListener('mouseup', mouseup)
})

onUnmounted(() => {
document.body.removeEventListener('mousedown', mousedown)
document.body.removeEventListener('mouseup', mouseup)
})

return { isMousedown }
}

0 comments on commit e85225d

Please sign in to comment.