likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 22

前言

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

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

原本的效果:

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

现在需要的效果:

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

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

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

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

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

  • 正常速度点击

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

  • 快速来回点击

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

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

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

实现 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 DesignElement UI 中的复选框。

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

支持 v-model 双向绑定

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

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

效果展示

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

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

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

代码大致如下:

// 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 事件都不会触发,如下:

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

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

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

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

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

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

解决问题

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

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

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

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

既然 onChange 事件不受影响,那还要 onBluronFocus 干嘛?

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

在回头简单看下需求:

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

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

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

由于需要 onBlur 事件,但是又因为前面提到当快速点击时会存在不触发 onFocus 和 onBlur 事件的问题,因此我们才需要在 onChange 中去手动触发 checkboxonFocus 事件,只有这样,当我们在点击 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>
        )
    }
})

最终效果如下:

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

最后

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

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

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

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

转载自:https://juejin.cn/post/7276467933236658213
评论
请登录