Bros!使用 focus 和 blur 事件时别忽略了这一点!

前言

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

最近小伙伴又遇到一个需求,那就是给复选框选中时添加一个边框样式,大致如下:

原本的效果:

现在需要的效果:

这个外边框的隐藏时机为:

  • 复选框未选中
  • 点击 复选框内容外 的区域

小伙伴很快就完成了需求,但 测试同学反馈了如下的 BUG

  • 正常速度点击

  • 快速来回点击

很明显快速点击时 外部边框 并没有按预期效果进行 展示/隐藏,为了更好的复现问题,接下来重新实现一遍功能,然后再解决问题。

实现 checkbox 组件

由于这个 checkbox 使用的是内部业务组件,并且是通过 JSX 语法 来实现,所以这里就不使用 template 模版 语法了。

核心分析

内容比较简单,要自己实现一个 checkbox 核心无非以下几点。

使用 <label> 关联 <input />

  • 为了实现点击 文字部分 也能达到直接点击 checkbox 的效果,就可以通过 <label> 标签来实现,而关联方式又分为两种
    • <input>id<label>for 属性保持一致

      html 复制代码
        <label for="cheese">Do you like cheese?</label>
        <input type="checkbox" name="cheese" id="cheese" />
    • <label> 直接包裹 <input>

      html 复制代码
        <label>
            Click me 
            <input type="text" />
        </label>

自定义 checkbox 样式

为了统一展示样式,因此都会使用 span 元素 来替换 原始的 checkbox ,并且将 原始 checkbox 隐藏起来,例如 Ant DesignElement UI 中的复选框。

支持 v-model 双向绑定

自行封装的组件为了方便外部使用,需要支持 v-model 数据双向绑定的形式,而在组件内部只需要将对应的 propsemits 事件进行定义即可,如下:

js 复制代码
export default defineComponent({
    props: {
        modelValue: {
            type: Boolean,
            default: false
        }
    },
    emits: ["update:modelValue"],
    ...
})

效果展示

如下的效果中没有展示鼠标点击位置,实际点击位置包括:

  • checkbox 本身 ,展示选中样式,包括 外边框填充样式
  • checkbox 文字部分 ,隐藏 外边框
  • checkbox 外的区域 ,隐藏 外边框

代码大致如下:

js 复制代码
// ChcekBox.tsx
import { defineComponent, ref } from 'vue'

export default defineComponent({
    name: 'ChcekBox',
    props: {
        modelValue: {
            type: Boolean,
            default: false
        }
    },
    emits: ["update:modelValue"],
    setup(props, { slots, emit }) {
        const blur = ref(false);

        const onChange = (e) => {
            emit('update:modelValue', e.target.checked)
        }

        const onFocus = () => {
            blur.value = false;
        }

        const onBlur = () => {
            blur.value = true;
        }

        return () => (
            <label class="checkbox-wrapper">
                <span class={["checkbox", props.modelValue && !blur.value && "checkbox-checked"]}>
                    <input
                        class="checkbox-input"
                        type="checkbox"
                        name="checkbox"
                        checked={props.modelValue}
                        onBlur={onBlur}
                        onFocus={onFocus}
                        onChange={onChange}
                    />
                    <span class={["checkbox-inner", props.modelValue && "checkbox-inner-checked"]}></span>
                </span>
                <span class="checkbox-label">{slots.default ? slots.default() : ''}</span>
            </label>
        )
    }
});

// ChcekBox.less
.checkbox-wrapper {
  display: flex;
  align-items: center;
  cursor: pointer;

  .abs {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    height: 16px;
    width: 16px;
  }

  .checkbox {
    position: relative;
    margin-right: 5px;
    height: 16px;
    width: 16px;
    padding: 10px;
    border-radius: 4px;
    border: 1.5px solid transparent;

    &-checked{
        border-color: #1677ff;
    }

    &-inner {
      .abs();
      border: 1px solid #1677ff;
      border-radius: 4px;

      &-checked {
        background-color: #1677ff;
        border-color: #1677ff;
      }

      &::after {
        box-sizing: border-box;
        position: absolute;
        top: 50%;
        inset-inline-start: 21.5%;
        display: table;
        width: 6px;
        height: 9px;
        border: 2px solid #fff;
        border-top: 0;
        border-inline-start: 0;
        content: " ";
        transform: rotate(45deg) scale(1) translate(-50%, -50%);
        transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
      }
    }

    &-input {
      .abs();
      opacity: 0;
    }
  }
}

分析/解决 外边框展示 BUG

再来回顾下 快速来回点击 时的展示效果:

然后回顾以上实现的代码中,不难发现是想通过 checkbox 上的 onFocusonBlur 事件被触发时来控制 外边框显示/隐藏

那么现在 显示/隐藏 出现问题,很明显和 onFocusonBlur 事件是有关系的。

focus 和 blur 事件

focus 事件在元素 获取焦点 时触发,该事件 不可取消 ,也 不会冒泡

blur 事件在一个元素 失去焦点 时被触发,该事件 不可取消 ,也 不会冒泡

还有一个最容易被忽略的点,那就是它们必须由 另一状态 变化到 当前状态 时才会被触发,例如:

  • 本身处于 聚焦状态 时,继续进行 聚焦动作 时,不会触发 focus 事件
  • 本身处于 失焦状态 时,继续进行 失焦动作 时,不会触发 blur 事件

分析问题

首先从布局上来讲 元素 <input class="checkbox-input" />元素 <span class="checkbox-inner"> 都使用了 绝对定位 position ,并且 后者 会覆盖在 前者 之上。

事件冒泡

那么我们现在的点击操作实际上直接操作的是 元素 <span class="checkbox-inner"> ,那么为什么还能触发在 元素 <input class="checkbox-input" /> 绑定的 focusblur 事件呢?

这个倒也不难,因为有 事件冒泡 ,所以这个点击操作能被传递到底层的 <input /> 元素,自然就可以触发相应的事件。

focus 和 blur 不触发

当进行 快速点击 时,会发现 focusblur 事件都不会触发,如下:

很明显就是因为这两个事件没有执行而导致展示有问题。

那么疑问是什么这两个事件没有被执行呢?

如果从状态变更上来说,最后一次触发的是 blur 事件,那么后续 blur 事件不触发是可以理解的,毕竟状态没有被改变,但是 focus 事件却没有被执行,这一点是不应该的。

关于这个问题暂未查询相关资料,有知道的掘友可以在评论区分享见解。

解决问题

知道了原因,那么就很容易进行调整了,虽然 focusblur 不能一直被触发,但是 change 事件却不受影响,如下:

因此,只需要将 外边框的显示/隐藏 相关逻辑迁移到 onChange 中即可,如下:

  • 为了保证 onBlur 事件能够被正常执行,在 onChange 被触发时我们应该通过 e.target.focus() 去触发 外边框显示逻辑 ,目的是改变 checkbox 中的 聚焦/失焦 状态,正如前面说的只有 当前状态下一状态 不同才能触发相应事件
js 复制代码
import { defineComponent, ref } from 'vue'

export default defineComponent({
    name: 'ChcekBox',
    props: {
        modelValue: {
            type: Boolean,
            default: false
        }
    },
    emits: ["update:modelValue"],
    setup(props, { slots, emit }) {
        const blur = ref(props.modelValue);

        const onChange = (e) => {
            e.target.focus();
            emit('update:modelValue', e.target.checked)
        }

        const onFocus = () => {
            blur.value = false;
        }

        const onBlur = () => {
            blur.value = true;
        }

        return () => (
            <label class="checkbox-wrapper">
                <span class={["checkbox", props.modelValue && !blur.value && "checkbox-checked"]}>
                    <input
                        class="checkbox-input"
                        type="checkbox"
                        name="checkbox"
                        checked={props.modelValue}
                        onBlur={onBlur}
                        onFocus={onFocus}
                        onChange={onChange}
                    />
                    <span class={["checkbox-inner", props.modelValue && "checkbox-inner-checked"]} onClick={()=>console.log('click in checkbox-inner')}></span>
                </span>
                <span class="checkbox-label">{slots.default ? slots.default() : ''}</span>
            </label>
        )
    }
})

最终效果如下:

最后

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

综上所述,当需要在项目中使用 focusblur 事件实现相关需求时,要格外注意,特别是在 safari 浏览器IOS 设备 等环境中都可能会存在 focusblur 事件不生效的情况。

希望本文对你有所帮助!!!

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax