资讯专栏INFORMATION COLUMN

潜在的恶意:前端弹窗类组件的键盘操作

tinylcy / 652人阅读

摘要:而当用户使用键盘操作,焦点发生变化后,当前焦点在哪一项上,必须给出明确的视觉提示,否则极容易带来困难,甚至误操作。回车键按下时,则会到相应账号,或者取消操作关闭弹出层。再来看看组件库的组件中的确认消息。

本文作者:文蔺
原文地址:http://www.wemlion.com/2016/a...
本文由 @文蔺 创作,转载请保留此声明。
所有权利保留,请勿用于商业目的。

问题提出

需要说明的是,题目中所说的 Modal,指的是所有由前端开发者自定义的对话框,如通常用到的 Alert、Prompt、Confirm 等等,经常伴随着一个半透明的灰黑色全局 mask。

事情源自某天使用某网站的页面,出现一个自定义的 Comfirm,习惯性按下回车确认,等了很久也不见弹出层关闭。于是很绝望。继而发现,真不是这一个网站的问题。

看看一般浏览器原生的 alert、prompt、confirm 可以发现,它们基本上都提供默认操作项(eg.确认/取消),通过回车键可以直接完成。传统软件如 PhotoShop 也是如此,否则真不知那些无需鼠标的 PS 大神是怎样练出来的。然而不少自定义的插件,却并未提供这样的功能,往往不得不将手挪到鼠标或触摸板上操作,极其不便。

主要存在两个方面的问题,一是根本无法直接通过键盘操作对话框;二是虽然可以操作,但视觉提示并不明显;三是整个应用对焦点的控制不够,容易导致成本可能极高误操作。下面先简单谈谈这几个问题,然后结合实际例子说明。

键盘、鼠标转换成本

先谈第一个问题。键盘、鼠标之间的切换成本应该是很高的,尤其是当应用响应与用户心理预期不符时,造成的焦虑感或挫败感很影响用户体验。

因此在使用弹出 Modal 时,需要根据业务场景,设置一个默认选项(尤其是确认、取消、提交等操作),用户通过回车键或鼠标点击就可以轻松完成任务。不过,另外一种更好的方式是提供 Tab 和 (Shift + Tab) 两种操作,通过键盘可以自由地在弹出层不同选项之间按照顺序切换。

明确的焦点提示

接着转到第二个,视觉提示的问题。没有明确是视觉提示,会造成用户的疑惑。

有默认选项的时候,无论是通过闪动的光标、背景色的变化,还是边框、outline 等形式,都需要明确提示用户,Modal 弹出时默认操作项是什么。而当用户使用键盘操作,焦点发生变化后,当前焦点在哪一项上,必须给出明确的视觉提示,否则极容易带来困难,甚至误操作。

应用对焦点的控制

第三个问题实际是第二个问题的延续。前面提到焦点的视觉提示问题,其实还有一种可能性,可能导致严重后果,这就是用户通过 Tab 操作,将焦点移动到弹出层 mask 背后的应用中。

这时候,一方面没有给出清晰的提示,一方面没有对焦点进行很好的控制,万一焦点在一个敏感位置(如一个外部跳转链接),按下回车键,这时候页面将刷新,用户此前的数据面临荡然无存的境地。脑洞再大一点,万一 mask 背后获得焦点的是巨额交易乃至核按钮(233333),那么恭喜你,攻城狮生涯就此结束。

我们知道,当网页弹出原生 alert 时,整个页面其他部分处于不可操作状态。使用类似的自定义组件替代这些功能时,纵然我们不能阻塞页面其他所有部分,但也得采取一定的控制,防止意外发生,最简便的方法便是将 Tab 的控制权掌握在开发者手中。

当然,相比于第三点,前两点应该是必须项,第三项可以算是加分项,在一些普通应用中不实现影响也不大。

举栗子

我们来看下一些实现。

首先是 Github 的 Fork。如图所示:

首先 fork 操作不提供默认选项。但使用 Tab 和 (Shift + Tab) 时,焦点只会在整个弹出层中用紫色框线标出的四个部分中依次切换。回车键按下时,则会 fork 到相应账号,或者取消操作、关闭弹出层。

接着看 SweetAlert,以第一个 Confirm 为例:

Confirm 弹出后,默认选项是右边的 “Yes, delete it!”,点击回车键,就会产生确认反馈。通过 Tab 操作几次发现,整个页面的焦点,只能够在图中两个按钮之间切换。同时仔细观察还能,获得焦点状态(:focus)的按钮,会有一个不太容易察觉的 box-shadow。

再来看看 Vue 组件库 Element 的 MessageBox 组件 中的 “确认消息” Demo。

先说结果,嗯,有提供默认选项,切换到不同选项时,也会有视觉提示(“取消”按钮的颜色和边框色会变,“确认”按钮背景亮度有明显变化)。然而有两点做得还不够:第一,无法通过 Tab 轻易切换到“❎”关闭按钮;第二点,也是最致命的是,根本就没有获取到 Tab 的控制权,以至于多按几次 Tab 然后回车,页面跳到主页,而弹框依然存在(如图所示)。

和 Element 类似,Weui 中的 Dialog 也存在类似问题,但 Weui 主要面向移动端,倒也情有可原。

如何实现控制

一开始我想到了 tabIndex 这个属性。但这货控制起来,十分不方便,在本文场景中使用它,简直是惹火烧身。

那么我能想到最简单的实现,莫过于手动调用 focus 事件了。 Simple Demo 。代码示例如下:

var $ = (sel) => document.querySelector(sel);

$("#js-show-alert").addEventListener("click", () => {
  $("#js-prompt").style.display = "block";
  $("#js-confirm").focus();
});

$("#js-cancel").addEventListener("click", () => {
  if (confirm("Are your sure?")) {
    $("#js-prompt").style.display = "none";
  }
});

$("#js-confirm").addEventListener("click", () => {
  $("#js-prompt").style.display = "none";
  alert("Confirmed! The Prompt will disappear");
});

样式方面,按钮使用的是 button,整体并未 reset,所以按钮获取焦点时,是有 outline 的。这也提醒我们,可以直接通过 :focus 伪类来实现 tab 选中时的样式,这样做好懂又靠谱。

还存在另外一点问题,上面的代码中,#js-confirm 元素是默认获取焦点的。那么,假如我们不需要任何默认焦点,而又需要用户按下 Tab 时,让 #js-prompt 中的按钮依次获得焦点呢?

这里提一点不成熟的想法(23333333)。只要让容器元素 #js-prompt 获得焦点即可:$("#js-prompt").focus();。在控制台上打印下 HTMLElement.prototype.focus,就会明白我的意思了。但这么做,可能是野路子(我还没细看 HTML 中元素获得焦点相关的规范)。

但是,上面 Demo 的实现,没有考虑到 Tab 控制权的接管。要接管 Tab,就得使用事件绑定来完成。下面看下 Github 是如何实现的。

如何取得控制权

找到 Github 对应的源码,又通过开发者工具发现弹出层容器有个值为 facebox 的 id。

接下来,开始 Ctrl + F 大法。利用长久积累的火眼金睛,找到下面这样两段代码:

就是它了。

先看第二段,整理后如下,重点直接划在注释中:

// 弹出层出现
document.addEventListener("facebox:reveal", function() {
  var t = document.getElementById("facebox");
  setTimeout(function() {
    e(t)
  }, 0);

  // 为 document 绑定 keydown 事件
  // 事件回调函数 n 就是第一张截图的函数
  r(document).on("keydown", n);
});

// 弹出层关闭后
document.addEventListener("facebox:afterClose", function() {
    // 移除出现时候为 document
    // 绑定的 keydown 事件
    r(document).off("keydown", n);

    // 弹出中已经获得焦点的元素
    // 强行失去焦点
    r("#facebox :focus").blur()
});

// ....

再来看看函数 n 做了什么。

function n(e) {
  var t = void 0;

  // hotkey 是自定义的
  // 这里开始控制 tab 和 shift+tab 操作
  if ("tab" === (t = e.hotkey) || "shift+tab" === t) {
    // 阻止默认事件
    e.preventDefault();

    // 弹出层
    var n = r("#facebox"),

      // 变量 i 存储弹出层中可以获得焦点的、可见的、可用的
      // input, button, .btn, textarea 等元素集合
      // filter 用到的函数 o 用于过滤不可见的相关元素
      // o = require("github/visible")["default"]
      i = r(Array.from(n.find("input, button, .btn, textarea")).filter(o)).filter(function() {
          return !this.disabled
      }),

      // tab 方向
      s = "shift+tab" === e.hotkey ? -1 : 1,

      // i 集合中当前获得焦点的元素的 index
      a = i.index(i.filter(":focus")),

      // i 集合中下一个获得焦点的元素的位置
      u = a + s;
    
    // 如果下一个获得焦点的位置超出 i 集合范围
    // 或者当前没有任何元素获得焦点、并且使用的是向前的 tab 键
    // 则将焦点置于 i 集合中的第一个元素上
    if (u === i.length || -1 === a && "tab" === e.hotkey) {
      i.first().focus()
    }
    // 当前没有任何元素获得焦点
    // 并且使用 shift+tab 向前
    // 则将焦点置于 i 集合中最后一个元素
    else if(-1 === a){
      i.last().focus();
    }
    // 正常行为
    // 将焦点置于下一个元素
    else {
      i.get(u).focus();
    }

  }
}

好了,本文也就结束了。

嗯,今天(2016-12-04)我想起来那个完全无法使用的例子了:

本文作者:文蔺
原文地址:http://www.wemlion.com/2016/a...
转载请注明来源

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91228.html

相关文章

  • 移动弹窗基础知识浅析——IOS弹窗体系

    摘要:尤其是遇到二次确认等场景因此,打算从头整理移动弹窗的基础知识,以弹窗体系为切入点,从定义出发,对移动弹窗进行分类,然后分别分析每一类弹窗的应用场景,以及在使用过程中需要注意的点。 摘要: 最为常见的【弹窗】反而是最捉摸不定的东西。各种类型的弹窗傻傻分不清楚,不知道在什么场景下应该用哪种弹窗。尤其是遇到二次确认等场景…… 因此,打算从头整理移动弹窗的基础知识,以iOS弹窗体系为切入点,从...

    jas0n 评论0 收藏0
  • 仿 taro-ui 实现 modal 组件 小程序组件

    摘要:其他的项目使用了拼装样式验证传入的属性是否是函数验证父组件传入的数据格式是否正确五参考文献谈谈的使用使用场景 仿 taro-ui 实现 modal 组件 小程序组件. 简介: 项目中使用到弹窗类的组件,重新制造了一个轮子. 源码地址: https://github.com/xiangxiong... 编写完modal组件总计花了28分钟. 效果图: showImg(htt...

    qpal 评论0 收藏0
  • 仿 taro-ui 实现 modal 组件 小程序组件

    摘要:其他的项目使用了拼装样式验证传入的属性是否是函数验证父组件传入的数据格式是否正确五参考文献谈谈的使用使用场景 仿 taro-ui 实现 modal 组件 小程序组件. 简介: 项目中使用到弹窗类的组件,重新制造了一个轮子. 源码地址: https://github.com/xiangxiong... 编写完modal组件总计花了28分钟. 效果图: showImg(htt...

    helloworldcoding 评论0 收藏0

发表评论

0条评论

tinylcy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<