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


解决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事件的冒泡特性使其能够捕获到组件内部任何子元素失去焦点的事件,并将其传递给父容器。

度加剪辑 度加剪辑

度加剪辑(原度咔剪辑),百度旗下AI创作工具

度加剪辑 359 查看详情 度加剪辑

修正后的代码示例:

<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事件。

注意事项与最佳实践

  1. 事件冒泡与捕获:理解事件冒泡和捕获机制是处理DOM事件的关键。focusout事件利用了冒泡阶段来向上通知父元素。
  2. mousedown vs click:在选项列表的li元素上,使用了@mousedown而不是@click。这是为了确保在选项被点击时,focusout事件不会过早地触发并关闭列表。mousedown事件在focusout之前触发,允许我们处理选项选择逻辑,然后才处理焦点离开组件的逻辑。
  3. 避免全局监听:在原始代码中,有一个document.addEventListener("click", ...)的全局监听器。在大多数情况下,对于组件内部的焦点管理,应尽量避免这种全局监听,因为它可能与组件自身的事件处理逻辑冲突,导致难以调试的问题。focusout事件提供了一种更局部化和高效的解决方案。
  4. 可访问性(Accessibility):正确使用tabindex和焦点管理对于确保组件的可访问性至关重要,特别是对于依赖键盘导航的用户。

总结

在Vue自定义组件中,当需要检测整个组件何时失去焦点时,blur事件因其不冒泡的特性而无法满足需求。通过将事件监听从@blur切换到@focusout,并确保父容器具有tabindex属性使其可聚焦,我们可以有效地实现组件的焦点管理。focusout事件的冒泡机制使其成为处理复杂交互式组件焦点状态变化的强大而可靠的工具,从而提升用户体验和组件的健壮性。

以上就是解决Vue自定义多选组件中Blur事件失效问题:Focusout的妙用的详细内容,更多请关注其它相关文章!


# vue  # css  # 汤姆影视网站建设  # 宜昌餐饮网站推广开户  # 企业网站优化概念界定  # 最大seo公司  # 嘉定营销推广电话多少  # 常平镇网站优化方案  # 江门网络seo机构  # 巩义网站建设团队招聘  # 哪个网站营销推广好  # 开源企业网站推广服务  # 自适应  # 全选  # 正确地  # 网页设计  # 双击  # 我们可以  # 输入框  # 使其  # 多选  # 自定义  # red  # overflow  # html元素  # 工具  # 事件冒泡  # access  # v-if  # app  # html 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: 抖音小程序怎么开通?小程序开通条件是什么?  J*aScript对象中深度嵌套URL键的查找与更新策略  如何在CSS中使用过渡制作按钮边框渐变_border-color transition实现  苹果手机怎么合并照片_苹果手机合并多张照片的操作方法  WPS文字如何进行简繁转换  《荔枝fm》导出文件教程  PHP与SQL实践:高效实现数据复制与特定列值修改  如何查找哪个composer包引入了特定的依赖?  行者app怎样导出日志  Dash应用多值文本输入处理与类型转换教程  菜鸟驿站的取件码忘了怎么办 手机快速查询指南  《淘宝联盟》推广自己的店铺方法  使用 .htaccess 正确配置 WordPress 子目录重定向与路径保留  解决异步Python机器人中同步操作的阻塞问题  AO3中文版手机快速通道_AO3最新稳定链接更新  京东快递物流信息不更新怎么办_物流停滞原因与处理方法  c++如何链接Boost库_c++准标准库的集成与使用  b站网页版入口 哔哩哔哩官方网站直接进入  《洛克王国:世界》国家队搭配攻略  5G和6G的连接密度有什么区别 6G每平方公里能连接多少设备  《小宇宙》标记不友善评论方法  折叠屏手机充不进电是什么问题? 特殊结构带来的维修难点  《红果免费短剧》下载观看方法  我的世界游戏平台入口 我的世界官方官网直达链接  如何在CSS中使用伪类选择器_hover实现悬停效果  iPhone14开启Apple TV遥控设置  智学网成绩单查询系统网_智学网学生平台登录  铁路12306官网入口 铁路12306中国铁路官网登录首页  抖音赚钱快速入门_新手必看的抖音赚钱步骤  哔哩哔哩在线观看入口 B站官网免费进入  抖音号升级成企业资质怎么弄?有什么好处?  J*aScript类型数组_TypedArray使用  192.168.1.1路由器后台入口 192.168.1.1默认登录入口  全球各国上班时间表外贸邮件时间  WooCommerce 购物车:始终显示所有交叉销售商品  店铺如何做视频号推广?做视频号推广有用吗?  《爱笔思画x》魔棒工具抠图教程  《磁力猫》最好用的磁官网  Google Drive API 认证:服务账户与OAuth 2.0的选择与实践  CSS如何在页面中引入重置样式_使用Normalize.css或Reset.css统一浏览器默认样式  解决SQLAlchemy模型跨文件关联的Linter兼容性指南  如何在 WordPress 前端实现内容提交:古腾堡编辑器的替代方案与实践  C++ static关键字作用_C++静态成员变量与静态函数  《王者荣耀世界》英雄获取攻略  PHP页面重载时变量值不重置的实现方法  123平台官方登录入口 123邮箱网页端在线沟通工具  PHP中获取HTTP响应状态消息:方法与限制  win11自带录屏文件保存在哪里 Win11 Game Bar录制视频默认路径【分享】  iSpring三分屏制作教程  @Team是什么?揭秘团队含义 

 2025-11-04

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.