新闻中心
Vue自定义多选组件中焦点事件处理:Blur与Focusout的深度解析

本文深入探讨了在vue自定义多选组件中处理焦点事件的常见问题。当组件内部输入框失去焦点时,外部容器的blur事件可能无法按预期触发,导致下拉列表无法关闭。核心问题在于blur事件不冒泡,而focusout事件则会冒泡。通过将blur替换为focusout,并确保容器可聚焦,可以有效解决此问题,实现组件外部点击时正确关闭选项列表的功能。
Vue自定义组件中焦点事件的挑战
在开发Vue自定义组件,特别是像多选下拉列表这类需要根据焦点状态控制UI元素(如选项列表)显示与隐藏的组件时,正确处理焦点事件至关重要。一个常见的需求是,当用户点击组件外部时,组件的选项列表应该自动关闭。开发者通常会尝试在组件的根元素上监听blur事件来实现这一逻辑。然而,在某些情况下,尤其当组件内部包含可聚焦的元素(如input字段)时,blur事件的行为可能不尽如人意。
具体来说,如果用户点击组件内部的input字段,然后点击组件外部的任何地方,组件根元素上的blur事件可能不会触发,导致选项列表仍然保持打开状态。这通常是由于对浏览器事件机制的误解造成的。
Blur与Focusout事件的根本区别
要理解上述问题并找到解决方案,我们需要深入理解blur和focusout这两个焦点相关事件的本质区别:
-
blur事件:
- blur事件在元素失去焦点时触发。
- 关键特性:不冒泡。 这意味着当一个子元素失去焦点时,它的blur事件不会传播到父元素。父元素只有在它自身直接失去焦点时才会触发blur事件。
- 在我们的多选组件场景中,当input字段失去焦点时,它会触发自己的blur事件,但这个事件不会向上冒泡到包含input字段的外部div。因此,外部div上的@blur="showOptions = false"不会被触发。
-
focusout事件:
- focusout事件也在元素失去焦点时触发。
- 关键特性:冒泡。 这意味着当一个子元素失去焦点时,它的focusout事件会向上冒泡到父元素,直到文档根部。
- 因此,当input字段失去焦点时,它会触发focusout事件,并且这个事件会冒泡到外部div,从而触发外部div上监听的focusout处理函数。
解决方案:使用focusout事件
鉴于blur事件不冒泡的特性,为了在父元素上捕获其子元素失去焦点的事件,我们应该使用focusout事件。
OneStory
OneStory 是一款创新的AI故事生成助手,用AI快速生成连续性、一致性的角色和故事。
319
查看详情
原始代码中的问题示例:
<div @blur="showOptions = false" :tabindex="tabIndex"> <!-- ... 组件内容,包括一个input字段 ... --> <input class="focus:outline-0 w-full" type="text" v-model="searchInput" /> <!-- ... --> </div>
在这个例子中,@blur="showOptions = false"绑定在外部div上。当用户在input字段中输入后,点击组件外部,input字段会失去焦点。但由于blur事件不冒泡,外部div不会收到这个失去焦点的通知,showOptions也就不会被设置为false。
修正后的代码示例:
将外部div上的@blur事件替换为@focusout。
<template>
<div class="flex flex-col relative w-full">
<span v-if="label" class="font-jost-medium mb-2">{{ label }}</span>
<div>
<!-- 将 @blur 替换为 @focusout -->
<div @focusout="showOptions = false" :tabindex="tabIndex">
<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=&
quot;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
class="hover:bg-primary-light px-4 py-2 rounded-md cursor-pointer"
role="option"
v-for="option in optionsMap"
@mousedown="onOptionClick(option)"
>
{{ 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 { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
export default defineComponent({
name: "AppAutocomplete",
components: {
MagnifyingGlassIcon,
},
props: {
modelValue: {
type: String,
},
label: {
type: String,
default: "",
},
tabIndex: {
type: Number,
default: 0, // 确保父div可聚焦
},
options: {
type: Array as PropType<{ text: string; value: string }[]>,
required: true,
},
},
setup(props, { emit }) {
const showOptions = ref(false);
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);
});
};
sortOptionsMapList();
return {
showOptions,
optionsMap,
searchInput,
selectedOptions,
onOptionClick,
onDeleteOption,
};
},
});
</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>注意事项与最佳实践
- tabindex属性: 确保你的父容器(即监听focusout事件的div)具有tabindex属性(例如tabindex="0"或tabindex="-1")。这使得该div元素能够接收焦点,从而在焦点离开它或其子元素时正确触发focusout事件。在提供的代码中,tabIndex prop已经确保了这一点。
- 事件冒泡的理解: 深入理解DOM事件的捕获和冒泡阶段对于开发交互式组件至关重要。blur和focus事件不冒泡,而focusin和focusout事件则会冒泡。
- 用户体验: 使用focusout可以提供更流畅的用户体验,因为无论用户是点击组件内部的可聚焦元素后离开,还是直接点击组件外部,选项列表都能一致地关闭。
- 可访问性: 正确处理焦点事件对于键盘导航和整体可访问性至关重要。确保组件在没有鼠标的情况下也能完全操作。
- @mousedown与@click: 在处理选项点击时,如果希望在focusout事件处理函数关闭选项列表之前捕获到点击事件,可以考虑使用@mousedown而不是@click。因为mousedown事件在blur/focusout之前触发,可以避免在点击选项时列表被过早关闭。在示例代码中,选项列表的li元素已经使用了@mousedown,这是一个很好的实践。
总结
在Vue自定义组件中,当需要父元素监听其内部子元素失去焦点的事件时,应优先使用focusout事件而非blur事件。focusout事件的冒泡特性使其能够捕获到子元素失去焦点的通知,从而实现更灵活和可靠的UI交互逻辑。同时,确保父容器具有tabindex属性是实现这一机制的必要条件。通过这些调整,可以显著提升自定义多选组件的用户体验和功能完整性。
以上就是Vue自定义多选组件中焦点事件处理:Blur与Focusout的深度解析的详细内容,更多请关注其它相关文章!
# 它会
# 衢州问答推广营销
# 网站建设培训找哪家
# 金融体系关键词排名前十
# 招标网站建设美丽中国
# seo利为汇培训
# 青海省网站建设设计报告
# 抖音seo价格如何计算
# seo推广管理
# 杭州化妆品网站建设
# 松原seo怎么样
# 其子
# 自己的
# 正确处理
# 如何做
# 则会
# css
# 这一
# 至关重要
# 多选
# 自定义
# red
# overflow
# 点击事件
# 常见问题
# 区别
# 事件冒泡
# v-if
# app
# 浏览器
# vue
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
css链接悬停下划线样式如何自定义_使用::after结合content和transition
在J*a项目里如何构建对象之间的契约_接口约束的实际落地
QQ邮箱网页版入口 QQ邮箱官方邮箱登录通道
Node.js CSV 数据处理:基于字段值条件过滤整条记录的策略
Pandas DataFrame:高效添加条件计算列
一加Ace 6T实拍样张首次公布!李杰:主摄实力完全看齐4K档性能旗舰
如何使用Go和Martini动态服务解码后的图片
QQ邮箱网页版入口登录 QQ邮箱在线邮箱官方通道
Golang如何实现Web接口签名验证_Golang Web接口签名校验开发方法
Mac终端命令大全_Mac常用Terminal指令速查
sublime如何只显示或隐藏特定类型文件_sublime侧边栏文件过滤
微信群消息显示延迟如何解决 微信群消息刷新优化方法
sublime如何优雅地处理行尾空格_sublime自动清理多余空白字符配置
c++ dfs和bfs代码 c++深度广度优先搜索算法
Python中高效且防溢出的双曲正弦计算:基于对数空间的优化策略
Lar*el 8 多关键词数据库搜索优化实践
如何在 Windows 11 中启动游戏手柄设置
QQ邮箱在线登录平台 QQ邮箱个人邮箱网页版入口
Basecamp怎样用留言钉固定重点_Basecamp用留言钉固定重点【重点标记】
Win11输入法不见了怎么办_Windows11恢复语言栏显示方法
C#使用XPath查询节点时出错? 常见语法错误与调试技巧
从OpenAI API响应中高效提取生成文本
Lar*el表单中优雅地处理“返回”按钮以规避验证:最佳实践指南
随机参数递归函数的基准调用次数与时间复杂度探究
如何在CSS中使用visited与link控制链接颜色_visited link伪类配合
UE5.7引擎表现爆炸优化无敌!5090跑4K稳定60FPS
PySpark中高效提取字符串右侧可变长度数字:使用regexp_extract
天猫2025双十一0点秒杀攻略 天猫爆款抢购时间
C++如何操作大型数据集_使用C++流式处理(Streaming)技术避免一次性加载大文件
现代化 SciPy 一维插值:interp1d 的替代方案与最佳实践
Yandex官方入口网址 Yandex俄罗斯搜索引擎最新在线地址
学习通在线学习平台 学习通网页版直接进入课程中心
动漫共和国防屏蔽稳定域名-动漫共和国官方正版直达通道
“在文档元素之后找到了标记”是什么错误? 检查并修复XML中多个根元素的3个方法
单12V-2×6实现为RTX 5090供电750W!甚至都没敢跑分
解决 MongoDB 聚合查询中对象数组 _id 匹配问题
HTML转PPT成品工具有哪些?HTML网页转PPT成品工具大全
小红书怎么解除第三方平台绑定_小红书多平台登录解绑方法介绍
12306选座怎么选到商务座_12306商务座选择与配置说明
Win10如何恢复误删的快捷方式_Win10重建常用软件快捷方式
谷歌google账号注册详细步骤 谷歌账号注册官方教程
Mac怎么查看崩溃日志_Mac控制台错误报告分析
斑马英语APP如何开启夜间护眼阅读_斑马英语APP夜间模式与低蓝光设置教程
利用Bokeh CustomJS动态控制DataTable列可见性
优化 Python 函数中的条件逻辑:解决 if-else 嵌套与参数选择问题
离线运行Go语言之旅:本地部署与GOPATH配置指南
b站怎么删除评论_b站评论管理与删除操作
想当下一个《2077》?《心之眼》Steam评价升至"多半好评"
Python中高效访问嵌套字典与列表中的键值对
QQ邮箱登录首页官网地址2026 QQ邮箱官方网页入口


2025-11-03
浏览次数:次
返回列表
quot;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
class="hover:bg-primary-light px-4 py-2 rounded-md cursor-pointer"
role="option"
v-for="option in optionsMap"
@mousedown="onOptionClick(option)"
>
{{ 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 { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
export default defineComponent({
name: "AppAutocomplete",
components: {
MagnifyingGlassIcon,
},
props: {
modelValue: {
type: String,
},
label: {
type: String,
default: "",
},
tabIndex: {
type: Number,
default: 0, // 确保父div可聚焦
},
options: {
type: Array as PropType<{ text: string; value: string }[]>,
required: true,
},
},
setup(props, { emit }) {
const showOptions = ref(false);
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);
});
};
sortOptionsMapList();
return {
showOptions,
optionsMap,
searchInput,
selectedOptions,
onOptionClick,
onDeleteOption,
};
},
});
</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>