自定义Vue多选组件中Blur事件无法正常工作
问题描述
自定义Vue多选组件存在Blur事件异常:
- 点击组件内除输入框外的区域时,外层div的Blur事件可正常触发以关闭选项列表;
- 点击输入框后再点击组件外部,该Blur事件无法触发,导致选项列表无法关闭;
- 要求:点击输入框后再点击组件外部时,外层div的Blur事件正常触发关闭列表;点击组件内选项列表时不触发该事件,因此不能仅在输入框上绑定Blur事件。
解决方案
核心思路是利用blur事件的relatedTarget属性,判断失去焦点后焦点是否转移到组件内部元素,只有当焦点转移到组件外部时才关闭选项列表:
- 添加组件根元素引用:给外层可聚焦的div添加ref,用于后续判断元素是否属于组件内部。
- 修改Blur事件处理逻辑:不再直接设置
showOptions = false,而是通过函数判断焦点去向:- 如果
relatedTarget是组件内部元素(包括输入框、选项列表),则不关闭列表; - 如果
relatedTarget是外部元素或null,则关闭列表。
- 如果
- 给选项列表元素添加tabindex:让选项li可以接收焦点,确保点击选项时
relatedTarget能正确指向该元素,被识别为内部元素。
修改后的完整代码
<template> <div class="flex flex-col relative w-full"> <span v-if="label" class="font-jost-medium mb-2">{{ label }}</span> <div> <!-- 添加ref,并修改blur事件处理函数 --> <div @blur="handleBlur" :tabindex="tabIndex" ref="componentRoot"> <div class="border border-[#EAEAEA] bg-white rounded-md flex flex-col w-full" > <div v-if="selectedOptions.length" class="flex flex-wrap px-4 py-2 border-b gap-2" > <div v-for="option in selectedOptions" class="border bg-secondary rounded-full py-1 px-2 flex items-center" > <span>{{ option.text }}</span> <vue-feather type="x" class="h-3 w-3 ml-1.5 cursor-pointer" @click="onDeleteOption(option)" /> </div> </div> <div class="flex flex-row justify-end items-center px-4 cursor-pointer" :class="selectedOptions.length ? 'py-2' : 'p-4'" @click="showOptions = !showOptions" > <MagnifyingGlassIcon class="h-5 w-5 mr-2" /> <input class="focus:outline-0 w-full" type="text" v-model="searchInput" /> <vue-feather type="chevron-down" class="h-5 w-5" /> </div> </div> <div v-if="showOptions && optionsMap.length" class="options-list"> <ul role="listbox" class="w-full overflow-auto"> <!-- 给li添加tabindex="-1",使其可接收焦点 --> <li class="hover:bg-primary-light px-4 py-2 rounded-md cursor-pointer" role="option" v-for="option in optionsMap" @mousedown="onOptionClick(option)" :tabindex="-1" > {{ option.text }} </li> </ul> </div> <div id="not-found" class="absolute w-full italic text-center text-inactive-grey" v-else-if="!optionsMap.length" > No records found </div> </div> </div> </div> </template> <script lang="ts"> import { defineComponent, PropType, ref, watch } from "vue"; import { IconNameTypes } from "@/types/enums/IconNameTypes"; import { AppIcon } from "@/components/base/index"; import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline"; export default defineComponent({ name: "AppAutocomplete", components: { AppIcon, MagnifyingGlassIcon, }, props: { modelValue: { type: String, }, label: { type: String, default: "", }, tabIndex: { type: Number, default: 0, }, options: { type: Array as PropType<{ text: string; value: string }[]>, required: true, }, }, setup(props, { emit }) { const showOptions = ref(false); // 定义组件根元素的ref const componentRoot = ref<HTMLElement | null>(null); const optionsMap = ref(props.options); const selectedOptions = ref<{ text: string; value: string }[]>([]); const searchInput = ref(""); watch(searchInput, () => { optionsMap.value = props.options.filter((option1) => { return ( !selectedOptions.value.some((option2) => { return option1.text === option2.text; }) && option1.text.toLowerCase().includes(searchInput.value.toLowerCase()) ); }); sortOptionsMapList(); }); const onOptionClick = (option: { text: string; value: string }) => { searchInput.value = ""; selectedOptions.value.push(option); optionsMap.value = optionsMap.value.filter((option1) => { return !selectedOptions.value.some((option2) => { return option1.text === option2.text; }); }); sortOptionsMapList(); emit("update:modelValue", option.value); }; const onDeleteOption = (option: { text: string; value: string }) => { selectedOptions.value = selectedOptions.value.filter((option2) => { return option2.text !== option.text; }); optionsMap.value.push(option); sortOptionsMapList(); }; const sortOptionsMapList = () => { optionsMap.value.sort(function (a, b) { return a.text.localeCompare(b.text); }); }; // 处理blur事件的函数 const handleBlur = (event: FocusEvent) => { const relatedTarget = event.relatedTarget as HTMLElement; // 判断relatedTarget是否是组件内部元素,如果不是则关闭选项列表 if (!componentRoot.value || !componentRoot.value.contains(relatedTarget)) { showOptions.value = false; } }; sortOptionsMapList(); return { showOptions, optionsMap, searchInput, selectedOptions, IconNameTypes, onOptionClick, onDeleteOption, componentRoot, handleBlur, }; }, }); </script> <style scoped lang="scss"> .options-list, #not-found { box-shadow: 0 0 50px 0 rgb(19 19 28 / 12%); @apply border border-[#EAEAEA] rounded-md p-4 mt-1 absolute bg-white z-10 w-full; } ul { @apply max-h-52 #{!important}; } </style>
关键说明
handleBlur函数通过componentRoot.value.contains(relatedTarget)判断焦点是否仍在组件内部,只有当焦点完全离开组件时才关闭选项列表;- 给选项li添加
tabindex="-1",确保点击选项时焦点会转移到li上,此时relatedTarget属于组件内部,不会触发列表关闭; - 输入框获取焦点时,
relatedTarget是输入框,属于组件内部,因此不会关闭列表,符合预期行为。
内容的提问来源于stack exchange,提问作者Kevin Garcia
相关产品推荐
相关产品推荐

