CSS动画导制的文本框失焦问题

674 阅读3分钟

在做一个手机端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 调试模式,切换到 图层 标签页. (默认没有的需要到更多工具里点开)

针对去掉动画前和去掉动画后两种情况来看一下他们的图层变化。

去掉动画前
  1. 在弹窗打开的时候:

    1. 会新增mask, dialog两个图层。
    2. 等fadeIn动画结束后,dialog图层会消失。

    也就是说一般这种绝对定位的元素, 浏览器会给它新开图层, 动画元素也会给它新开图层, 等动画结束后又会把图层删除.

  2. 关闭弹窗的时侯:

    1. 会新增 editor 图层 (因为focus的缘故) 和 dialog 图层。
    2. 等 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…

总结

这个问题让我对 图层 有了一点印象。

至于失焦的原因是不是真的跟图层删除有关,欢迎讨论。