在做一个手机端h5项目时遇到一个问题, 问题是关于文本框失焦的, 在此记录一下排查过程.
问题描述
页面是一个代码编辑器, 并且引入了 weui.js 作为弹窗组件. 为了简化场景, 我们用文本框来替代代码编辑器。
当用户在文本框中删除文本时, 在某些特定的条件下会触发弹窗, 让用户确认是否删除.
触发弹窗前我们会先让文本框失焦, 让软键盘先退下后再弹出弹窗. 当用户点击取消或者确认按钮后, 弹窗消失, 文本框要再次恢复聚焦.
<textarea id='editor' />
import weui from 'weui.js'
weui.confirm('确认删除吗?', () => {
document.getElementById('editor').focus();
})
现实的情况是文本框在focus之后又再次失焦了.
可以看看复现链接: stackblitz.com/edit/js-a4n…
手机端查看(可能需要vpn😔): js-a4nsdp.stackblitz.io/
源码复现
我们把 weui.js 中 dialog 的源码复制了过来, 方便 debug.
链接地址: stackblitz.com/edit/webpac…
排查原因
为什么关闭弹窗会引发失焦呢? 观察到失焦是在动画结束后发生的, 怀疑是动画的问题.
是不是 CSS 动画的问题
源码中在关闭弹窗时加了一个 weui-animate-fade-out 类, 在这个类里添加了一个 CSS 动画, 让 opacity 从 1 变到 0, 开始我怀疑是这个动画导致了重绘之类的原因.
/*
weui.js 中 dialog.js 里关闭弹窗的代码
可以看到它给mask和dialog加了一个类,这个类里定义了一个动画属性
等动画结束后移除弹窗元素.
*/
function _hide(callback) {
_hide = $.noop; // 防止二次调用导致报错
$mask.addClass('weui-animate-fade-out');
$dialog
.addClass('weui-animate-fade-out')
.on('animationend webkitAnimationEnd', function () {
$dialogWrap.remove();
_sington = false;
callback && callback();
});
}
改造_hide函数, 移除动画.
function _hide(callback) {
_hide = $.noop; // 防止二次调用导致报错
$dialogWrap.remove();
_sington = false;
callback && callback();
}
结果问题解决了, 移除了动画后, 当弹窗关闭, 焦点没有失去.
链接: stackblitz.com/edit/webpac…
为什么?
为什么移除了动画就好了呢? 如果说动画会重绘, 那关闭弹窗也会重绘啊.
先看一下弹窗的 html 结构.
<div>
<div class="weui-mask weui-animate-fade-in"></div>
<div class="weui-dialog weui-animate-fade-in" role="dialog" aria-modal="true" tabindex="-1">
<div class="weui-dialog__hd">
<strong class="weui-dialog__title">确定删除吗?</strong>
</div>
<div class="weui-dialog__bd"></div>
<div class="weui-dialog__ft">
</div>
</div>
</div>
mask 和 dialog 都是绝对定位
// mask
.weui-mask {
position: fixed;
z-index: 1000;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
// dialog
.weui-dialog {
width: 320px;
margin: 0 auto;
position: fixed;
z-index: 5000;
top: 50%;
transform: translateY(-50%);
}
图层
会不会是图层 (Layer) 的原因?
打开 chrome 调试模式,切换到 图层 标签页. (默认没有的需要到更多工具里点开)
针对去掉动画前和去掉动画后两种情况来看一下他们的图层变化。
去掉动画前
-
在弹窗打开的时候:
- 会新增mask, dialog两个图层。
- 等fadeIn动画结束后,dialog图层会消失。
也就是说一般这种绝对定位的元素, 浏览器会给它新开图层, 动画元素也会给它新开图层, 等动画结束后又会把图层删除.
-
关闭弹窗的时侯:
- 会新增 editor 图层 (因为focus的缘故) 和 dialog 图层。
- 等 fadeOut 动画结束后,mask 和 dialog 图层被删除,editor失焦,editor图层也被删除。
去掉动画后
弹窗打开时跟上面是一样的,在弹窗关闭的时候,mask图层直接被删除,又新增一个editor图层.
这么看起来,文本框失焦似乎跟图层被删除有关。而且是跟 editor 图层后面的图层被删除有关。
试试
按照这个逻辑,那只要让图层删除发生在 editor 图层前面就可以了, 也就是让文本框聚焦发生在关闭动画之后就可以了。因为源码的逻辑是先执行回调函数再关闭弹窗的。
//dialog.js 原来的逻辑
$dialogWrap
.on('click', '.weui-dialog__btn', function (evt) {
const index = $(this).index();
if (options.buttons[index].onClick) {
if (options.buttons[index].onClick.call(this, evt) !== false) hide();
} else {
hide();
}
})
// 改成下面这样,也就是把回调函数传给hide函数
$dialogWrap
.on('click', '.weui-dialog__btn', function (evt) {
const index = $(this).index();
if (options.buttons[index].onClick) {
const cb = options.buttons[index].onClick;
if (cb.call(this, evt) !== false) {
hide(() => cb.call(this, evt));
}
} else {
hide();
}
})
这样一改以后就可以了。
链接: stackblitz.com/edit/webpac…
总结
这个问题让我对 图层 有了一点印象。
至于失焦的原因是不是真的跟图层删除有关,欢迎讨论。