IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    熊的猫发表于 2023-10-17 14:56:19
    love 0

    前言

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

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

    原本的效果:

    image.png

    现在需要的效果:

    image.png

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

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

    06CE110E.gif

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

    • 正常速度点击

      1.gif

    • 快速来回点击

      1.gif

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

    08671C34.jpg

    实现 checkbox 组件

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

    核心分析

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

    使用 <label> 关联 <input />

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

      • <input> 的 id 与 <label> 的 for 属性保持一致

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

           <label>
            Click me 
            <input type="text" />
           </label>

    自定义 checkbox 样式

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

    image.png

    支持 v-model 双向绑定

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

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

    效果展示

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

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

    1.gif

    代码大致如下:

    // 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

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

    1.gif

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

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

    focus 和 blur 事件

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

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

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

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

    分析问题

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

    事件冒泡

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

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

    focus 和 blur 不触发

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

    1.gif

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

    071C7D88.gif

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

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

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

    解决问题

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

    1.gif

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

    • 为了保证 onBlur 事件能够被正常执行,在 onChange 被触发时我们应该通过 e.target.focus() 去触发 外边框显示逻辑,目的是改变 checkbox 中的 聚焦/失焦 状态,正如前面说的只有 当前状态 和 下一状态 不同才能触发相应事件
    既然 onChange 事件不受影响,那还要 onBlur 和 onFocus 干嘛?

    这个也是评论区掘友提出的疑问,为了避免大家都有这个疑问,统一在这里解释。

    在回头简单看下需求:

    • 点击 checkbox 本身,展示选中样式,包括 外边框 和 填充样式
    • 点击 checkbox 外的区域,隐藏 外边框

    那么我们知道,当点击 checkbox 本身 时是会一直触发 onChange 事件,从这个角度来讲确实不需要 onBlur 和 onFocus。

    但值得注意的是,当需要再点击 checkbox 外的区域 隐藏外边框时,onChange 事件就无法执行了,因为此时 checkbox 选中状态是没有发生变更的,而这个操作就很适合 onBlur 事件的触发时机,所以我们需要 onBlur 事件。

    由于需要 onBlur 事件,但是又因为前面提到当快速点击时会存在不触发 onFocus 和 onBlur 事件的问题,因此我们才需要在 onChange 中去手动触发 checkbox 的 onFocus 事件,只有这样,当我们在点击 checkbox 外的区域 时才能触发其 onBlur 事件。

    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>
            )
        }
    })

    最终效果如下:

    1.gif

    最后

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

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

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

    image.png



沪ICP备19023445号-2号
友情链接