新闻中心
解决Vue自定义多选组件中Blur事件失效问题:Focusout的妙用

在vue自定义多选组件中,当组件内部包含可聚焦元素(如输入框)时,直接在父容器上使用`blur`事件可能无法按预期触发,导致组件失去焦点时无法执行相应逻辑(例如关闭选项列表)。本文将深入解析`blur`事件不冒泡的特性,并提出使用`focusout`事件作为替代方案,详细阐述其工作原理及在复杂组件中实现正确焦点管理的方法。
理解Blur与Focusout事件
在Web开发中,处理用户界面焦点变化是常见的需求,尤其是在构建复杂的自定义组件时。blur和focusout是两种与元素失去焦点相关的事件,但它们在行为上存在关键差异:
blur事件:当一个元素本身失去焦点时触发。这个事件不会冒泡。这意味着,如果你在一个父元素上监听blur事件,但焦点从其子元素转移到父元素外部,父元素上的blur事件不会被触发。在多选组件的场景中,如果用户点击了组件内的输入框,然后点击组件外部,由于输入框失去焦点时blur事件不会冒泡到外层容器,导致父容器无法感知到焦点已离开整个组件。
focusout事件:当一个元素或其任何后代元素失去焦点时触发。这个事件会冒泡。这意味着,即使是子元素失去了焦点,focusout事件也会沿着DOM树向上冒泡,直到根元素。这使得focusout成为在父容器上监听整个组件焦点状态变化的理想选择。
原始问题分析
考虑一个自定义的多选组件,其结构包含一个外部div用于包裹整个组件,内部有输入框、已选选项列表和待选选项列表。组件希望在用户点击组件外部时关闭选项列表。最初的实现可能尝试在外部div上使用@blur="showOptions = false":
<div @blur="showOptions = false" :tabindex="tabIndex"> <!-- ... 组件内部结构,包括一个 <input> 元素 ... --> </div>
然而,这种实现会遇到一个问题:如果用户首先点击了组件内的input输入框,然后再点击组件外部的任何地方,外部div上的blur事件并不会被触发。这是因为当input失去焦点时,其blur事件不会冒泡到父div。因此,showOptions状态不会被更新,选项列表会保持打开状态。
解决方案:使用Focusout事件
为了解决blur事件不冒泡的问题,我们可以将外部div上的@blur事件替换为@focusout事件。focusout事件的冒泡特性使其能够捕获到组件内部任何子元素失去焦点的事件,并将其传递给父容器。
OneStory
OneStory 是一款创新的AI故事生成助手,用AI快速生成连续性、一致性的角色和故事。
319
查看详情
修正后的代码示例:
<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="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 { 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, // 确保 tabIndex 存在且有效
},
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();
// 移除不必要的 document.addEventListener("click"),因为它会干扰组件内部的焦点管理
// document.addEventListener("click", () => {
// console.log(document.activeElement
);
// });
return {
showOptions,
optionsMap,
searchInput,
selectedOptions,
IconNameTypes,
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>在上述代码中,关键的改动是将@blur="showOptions = false"修改为@focusout="showOptions = false"。现在,无论用户是直接点击了父div外部,还是先点击了组件内的input,然后将焦点移出组件,focusout事件都会在父div上触发,从而正确地关闭选项列表。
tabindex属性的重要性
在上述解决方案中,父div上使用了:tabindex="tabIndex"属性。tabindex属性在这里扮演了重要角色:
- 使非交互元素可聚焦:默认情况下,div元素是不可聚焦的。tabindex属性允许将任何HTML元素设置为可聚焦,使其可以通过键盘(Tab键)进行导航。
- 确保focusout在父元素上生效:为了让父div能够接收到焦点事件(包括focusout),它本身必须是可聚焦的。通过设置tabindex="0"(或任何非负整数),我们确保了div可以接收焦点,从而能够正确地监听和响应focusout事件。
注意事项与最佳实践
- 事件冒泡与捕获:理解事件冒泡和捕获机制是处理DOM事件的关键。focusout事件利用了冒泡阶段来向上通知父元素。
- mousedown vs click:在选项列表的li元素上,使用了@mousedown而不是@click。这是为了确保在选项被点击时,focusout事件不会过早地触发并关闭列表。mousedown事件在focusout之前触发,允许我们处理选项选择逻辑,然后才处理焦点离开组件的逻辑。
- 避免全局监听:在原始代码中,有一个document.addEventListener("click", ...)的全局监听器。在大多数情况下,对于组件内部的焦点管理,应尽量避免这种全局监听,因为它可能与组件自身的事件处理逻辑冲突,导致难以调试的问题。focusout事件提供了一种更局部化和高效的解决方案。
- 可访问性(Accessibility):正确使用tabindex和焦点管理对于确保组件的可访问性至关重要,特别是对于依赖键盘导航的用户。
总结
在Vue自定义组件中,当需要检测整个组件何时失去焦点时,blur事件因其不冒泡的特性而无法满足需求。通过将事件监听从@blur切换到@focusout,并确保父容器具有tabindex属性使其可聚焦,我们可以有效地实现组件的焦点管理。focusout事件的冒泡机制使其成为处理复杂交互式组件焦点状态变化的强大而可靠的工具,从而提升用户体验和组件的健壮性。
以上就是解决Vue自定义多选组件中Blur事件失效问题:Focusout的妙用的详细内容,更多请关注其它相关文章!
# 我们可以
# 威海网站推广定制
# 崇义seo优化加盟
# 网站推广渠道 联系q
# 宜宾营销推广报价
# 江苏网站建设哪里的好找
# seo 具体工作
# 做开推广网站
# 汽车4s店营销推广策略
# 辽宁seo排名团购网
# 培训网站建设讲义
# 是在
# 使用了
# 这是
# 如何做
# 正确地
# css
# 输入框
# 使其
# 多选
# 自定义
# red
# overflow
# html元素
# 工具
# 事件冒泡
# access
# v-if
# app
# html
# vue
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
提升屏幕阅读器对“m”时间单位的播报准确性:HTML与CSS组合解决方案
PDO预处理语句中冒号的正确处理:区分SQL函数格式与命名占位符
离线运行Go语言之旅:本地部署与GOPATH配置指南
蛙漫移动版在线看 蛙漫手机浏览器直达入口
c++中为什么推荐使用using替代typedef_c++现代化类型别名
qq游戏免费畅玩入口_qq游戏电脑版快速启动
没有大陆身份证/银行卡如何实名微信? 亲测有效的几种方法分享
UE5.7引擎表现爆炸优化无敌!5090跑4K稳定60FPS
“在文档元素之后找到了标记”是什么错误? 检查并修复XML中多个根元素的3个方法
C++如何比较两个字符串_C++ string compare函数与操作符对比
Safari自带网页翻译功能怎么用 无需插件轻松看懂外文网站【方法】
Go语言中Map存储的结构体如何调用指针方法:深入解析与实践
CSS条件样式无法按设备触发怎么排查_media条件语句正确设置解决触发问题
KFC套餐升级怎么获取优惠代码_KFC套餐升级活动与优惠代码获取方法
在J*a中如何开发简易博客标签推荐系统_博客标签推荐项目实战解析
MongoDB聚合管道:正确匹配对象数组中_id的方法
html5 app怎么运行环境_配html5 app运行环境【教程】
Lar*el用户头像管理:实现图片缩放、存储与旧文件安全删除的最佳实践
QQ邮箱网页版快速登录 QQ邮箱邮箱账号官方入口地址
微博网页版首页入口 微博电脑端官网登录链接
押井守高度称赞《辐射4》:玩了八年都停不下来!
Archive of Our Own官网直达 AO3最新可用地址一览
React/Next.js中实现列表项的动态选择与移动
快手赚钱渠道_快手收益来源
2025俄罗斯Yandex最新入口 官方网站地址及浏览器下载指南
理解Python模块与全局变量的作用域管理
MinIO大规模对象列表性能瓶颈深度解析与外部元数据管理策略
在Runstone环境中高效处理TasteDive API的JSON数据
漫蛙manwa官网登录界面_漫蛙漫画网页版主站入口
Win11怎么查看电脑配置_Win11硬件配置检测工具使用
天猫2025双十一0点秒杀攻略 天猫爆款抢购时间
PHP中高效并行检查多链接状态的教程
解决Django多数据库/多Schema环境下外键迁移问题
怎样把文件彻底粉碎无法恢复_Windows下安全删除敏感数据【隐私保护】
AO3最新入口2025公告_AO3中文官网合集
sublime侧边栏怎么增强功能_SideBarEnhancements for sublime安装与配置
企业名称高精度匹配:N-gram方法在结构相似性分析中的应用
斑马英语APP如何开启夜间护眼阅读_斑马英语APP夜间模式与低蓝光设置教程
Kafka Streams中基于消息头条件过滤消息的实现指南
网易大神怎么保存别人动态的图片_网易大神动态图片保存方法
Lar*el Excel导入时生成自定义递增ID的策略与实践
b站如何看历史记录_b站观看历史找回方法
新三国志曹操传110级星符试炼夏侯渊极难攻略
J*aScript动态修改指定div内所有a标签样式指南
CSS自定义字体样式被系统字体替换怎么办_font-face方式指定font-display控制渲染策略
Gmail邮箱申请注册直达_Gmail邮箱免费注册PC版官网入口2025
Go语言中的*string:深入理解字符串指针
css滚动动画效果怎么实现_使用Animate.css滚动触发动画类
AI泡沫首次被“刺破”:GPU十年都无法存活!
TikTok搜索结果不显示如何解决 TikTok搜索刷新优化方法


2025-11-04
浏览次数:次
返回列表
);
// });
return {
showOptions,
optionsMap,
searchInput,
selectedOptions,
IconNameTypes,
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>