前言
欢迎关注同名公众号《熊的猫
》,文章会同步更新,也可快速加入前端交流群!
最近小伙伴又遇到一个需求,那就是给复选框选中时添加一个边框样式,大致如下:
原本的效果:
现在需要的效果:
这个外边框的隐藏时机为:
- 复选框未选中
- 点击 复选框内容外 的区域
小伙伴很快就完成了需求,但 测试同学反馈了如下的 BUG:
正常速度点击
快速来回点击
很明显快速点击时 外部边框 并没有按预期效果进行 展示/隐藏,为了更好的复现问题,接下来重新实现一遍功能,然后再解决问题。
实现 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 中的复选框。
支持 v-model
双向绑定
自行封装的组件为了方便外部使用,需要支持 v-model 数据双向绑定的形式,而在组件内部只需要将对应的 props 和 emits 事件进行定义即可,如下:
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false
}
},
emits: ["update:modelValue"],
...
})
效果展示
如下的效果中没有展示鼠标点击位置,实际点击位置包括:
- checkbox 本身,展示选中样式,包括 外边框 和 填充样式
- checkbox 文字部分,隐藏 外边框
- checkbox 外的区域,隐藏 外边框
代码大致如下:
// 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 上的 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 事件都不会触发,如下:
很明显就是因为这两个事件没有执行而导致展示有问题。
那么疑问是什么这两个事件没有被执行呢?
如果从状态变更上来说,最后一次触发的是 blur 事件,那么后续 blur 事件不触发是可以理解的,毕竟状态没有被改变,但是 focus 事件却没有被执行,这一点是不应该的。
关于这个问题暂未查询相关资料,有知道的掘友可以在评论区分享见解。
解决问题
知道了原因,那么就很容易进行调整了,虽然 focus 和 blur 不能一直被触发,但是 change 事件却不受影响,如下:
因此,只需要将 外边框的显示/隐藏 相关逻辑迁移到 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>
)
}
})
最终效果如下:
最后
欢迎关注同名公众号《熊的猫
》,文章会同步更新,也可快速加入前端交流群!
综上所述,当需要在项目中使用 focus 和 blur 事件实现相关需求时,要格外注意,特别是在 safari 浏览器 或 IOS 设备 等环境中都可能会存在 focus 和 blur 事件不生效的情况。
希望本文对你有所帮助!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。