一个超级诡异的 iOS Safari `position: fixed` 失效问题

iOS Safari 下,`input readonly` 仍然可以获得输入焦点,此时页面交互大部分正常,但 `position:fixed` 会受影响。

今天前同事李某找我咨询 Hybrid 开发的问题,想起来大前天搞这个问题搞了一天,赶紧记下来,省得忘记。

先说需求。东家让我做个日历组件,在手机 Web 上用。组件的样式是这样的,很多地方都可以见到,比如南航国航的客户端。

日历控件需求图

看起来并不复杂,事实上也是,基本上顺顺利利的开发完成,准备交付。这里有个伏笔,开发中我按老习惯,使用桌面 Chrome,和实际生产环境不太一样。不过我自然要去真机上测试,结果一测问题就出来了。

因为组件需要全屏展示,所以我设置了如下的CSS:

.date-picker {
  position:fixed;
  top:0;
  left:0;
  right:0;
  bottom:0;
  background-color: white;
  z-index:1024;
}

同时,对原本的 <input name="date">,我给它加上 readonly,避免弹出虚拟键盘。理论上,这样的就可以了。但实测时,不滚屏的时候,组件弹出时尺寸是准确的,盖满全屏;然则一旦滚屏,组件就会占据从页面最上方到当前最下面这截位置。大约相当于 position:absolte;top:0 的效果。

Safari 截图
手机截图
如图,可以看到组件占据了全屏,但实际是从页面最上面开始的,定位有问题。用桌面 Safari 调试也可以看出来它的高度是 968,远大于正常的 667。

这很诡异,上下左右全为0,是上古巨兽 IE6 都支持的做法。iOS Safari 虽然 Bug 多多,不应该连这个都有毛病啊。以 ios safari position fixed 为关键词 Google 之,结果 iOS Safari 历史不清白,当年 iPhone 刚出的时候的确有定位问题,于是虽有满屏的结果,但都不适用。

然后我想到找其它库,比如 Bootstrap,它的 Modal 组件也是类似的效果。但是怎么测都正常,于是我只好一个样式一个样式修改,仍然没有结果。

时间慢慢流逝,转眼已经凌晨2点了,就在我几欲放弃之际,突然发现,虽然组件弹出的时候定位有问题,但只要我点掉下面的完成,定位就会立刻恢复正常。

手机截图
注意,就是那个“完成”。

问题至此已经明朗:在 iOS Safari 里,即使 <input> 设置了 readonly,它仍然可以获取输入焦点。获取输入焦点之后,虽然没有弹出虚拟键盘,但仍然是待输入状态。

此时页面各种交互都是正常工作的,比如点击、滚屏。唯独 position:fixed 定位有问题。点击“完成”离开输入状态,Safari 自动刷新页面元素,定位就正常了。

于是我在组件弹出后,自动 input.blur(),使其失去焦点,组件的尺寸便正常无误了。


总结

移动端 Web 开发总有各种各样稀奇古怪的问题。有些好解决,有些不好解决,比如这个问题,很难定位:

  1. 历史不清白,搜也搜不到
  2. 组件要求全屏,需要避免虚拟键盘,所以会改变默认行为
  3. 其它情况下都是好的

我能想到的方案,就是想办法,用所有能用的工具,排除掉所有其它问题,最终还是能搞出来的。

FontAwesome 通过组合创建新图标

复合使用 FontAwesome 图标,创建符合需求的新图标。比如把“图片(fa-picture-o)”和“加(fa-plus-circle)”叠到一起,就可以创造”新增图片“的图标。

FontAwesome 提供了很多好看的图标,使用 WebFont 嵌入页面更是简单又好用,所以我基本上一直用它。不过有时候还是觉得不太够用,这就需要复合使用多个图标。

下面是个例子,我在图标的右下角增加一个圆形加号,表示增加、创建。

See the Pen add icon by Meathill (@meathill) on CodePen.

这里有两个选择:

  1. FontAwesome 提供了一个堆叠图标的样式:fa-stack,可以堆叠任意多的图标。
  2. 因为它实际上只占用了 :before,所以也可以使用 :after 来容纳新增的图标。如果只需要两个图标,这个方式更简单。

Compass + cleancss 导致的灵异小问题

:blank伪类尚未被Chrome支持,会导致整条规则被忽略。

问题不大,不过很诡异。代码如下:

SASS:

.some-class
  display: none

.other-class:blank
  display: none

HTML:

<div class="some-class">...</div>

理论上说,这样这个div应该不显示。在本项目中,它的确没显示;但是在另外一个将本项目作为依赖的项目中,它却显示出来了。

经检查,本地开发和部署时,直接使用 compass compile 生成CSS,compass 配置中,设置输出模式(output_style)为 compressed,结果是这样的:

.some-class{display:none}.other-class:blank{display:none}

而在作为依赖时,用到的则是 grunt-contrib-cssmin 处理过的CSS,刚才那句就被压缩成

.some-class,.other-class:blank{display:none}

:blank伪类尚未被Chrome中支持,于是整条规则都被忽略,导致div显示出来。

[教程]纯CSS实现多选组件

使用最新的flexbox布局和CSS3新增选择器实现效果更好的多选项多选组件。

产品篇

在我们的后台中,需要设置广告精准投放的区域,也就是要在全国31个省、自治区、直辖市中选择。那么,出现下面这幅景象也就理所应当了:
1

这样做有几个问题:

  1. 选项很多,没有规律,找起来很累
  2. 如果是一个已经选择了部分选项的广告,修改时仍然需要用肉眼寻找,无法一眼看出来投放到哪些省份
  3. 选完一个,再选下一个,还要从头找,甚至会被已经选过的影响

于是我想,首先应该把所有选项分为“已选中”和“未选中”两批,解决第2个问题,减轻第3个问题;其次复选框本身的价值不大,可以被替换为其它样式;唯一可能引入的问题,就是点选时,用户的预期是看到复选框里出现一个小对勾,表示选中,如果我把它移开放到“已选中”组里,用户可能会迷惑,需要一些时间学习。

于是我跟某产品经理朋友聊了聊这个想法,他表示确实可能造成用户迷惑,不过如果能加入动画效果,那么基本没问题。嗯,开始动手。

技术实现篇

近日flexbox规范定案,浏览器相继支持display:flex;,同时传来一条好消息,新实现比老实现display:box;快很多。这次我打算用flexbox来解决问题,因为里面有一个很重要的属性:order(之前叫box-ordinal-group),它可以改变布局中元素的排列顺序,配合CSS3新增的选择器,应该可以满足需要。

第一步 分拆选中/未选中

(关于flexbox的知识,可以通过Google了解,虽然搜到的多是上一个版本,不过和最终版差别不大,只是叫法不同。本文不再过多讲解,我就当大家都会了)

<input type="checkbox">本身的样式不能修改,所以我们必须借助<label>的帮助;实现选中/未选中区分,那自然就要用到伪类:checked;选择器一定是从外到内、从前到后的,没法选择父级元素,所以不能用<label>去包<input>,那么最终布局就只能是:

<div>
    <input type="checkbox" name="q[]" id="q1" />
    <label for="q1">小宝3225</label>
    <input type="checkbox" name="q[]" id="q2" />
    <label for="q2">王老白白白</label>
    <input type="checkbox" name="q[]" id="q3" />
    <label for="q3">空夫31</label>
    <input type="checkbox" name="q[]" id="q4" />
    <label for="q4">谷大白话</label>
    <input type="checkbox" name="q[]" id="q5" />
    <label for="q5">Meathill</label>
    <input type="checkbox" name="q[]" id="q6" />
    <label for="q6">一毛不拔大师</label>
</div>

很简单哈,不解释了。CSS3新增了“下一节点”选择器 +,用来选择某节点的下一个节点,结合:checked伪类就可以将选中的<input>和它临近的<label>通过改变order属性移到前面去:

#container {
  display:flex;
  flex-direction:row;
  flex-wrap:wrap;
}
#container input,
#container label {
  order: 2; //所有选项、label顺序为2
}
input[type=checkbox]:checked,
input[type=checkbox]:checked + label {
  order: 0; // 越小越靠前
}

不过这样只是把选中的内容提前,视觉上没有真正的分割。所以我决定再加入一根分割线,上面是选中的,下面是未选的。这个时候我们需要用到 ~ 这个选择器,选择某节点后面的节点:

hr {
  display:none; // 默认情况下,没选任何选项,分割线隐藏
  order: 1; // 分割线顺序为1
  width:100%; // 保证独霸一行
}
input[type=checkbox]:checked ~ hr {
  display:block; // 有选项被选中后才会显示分割线
}

Demo如下:

这样基础功能实现了。不过视觉上,排版仍然不整齐,选中的选项和未选中的选项区分不算太明显,所以下一步我准备美化下checkbox。

第二步,美化checkbox

做法与前面类似,也要用到CSS3新增的选择器。前面为了实现<label>提前,没有用它包裹<input>,所以在选项很多很长导致换行的时候,可能出现复选框和标签脱离的尴尬状况。好在复选框的价值可以用别的样式取代,所以先把小方框隐藏起来,转而将<label>作为操作目标,再来点边框底色圆角(参考自Bootstrap 3),就可以了:

input[type=checkbox] {
  display: none;
}
label {
  min-width: 120px;
  border: 1px solid #CCC;
  padding: 2px 8px;
  text-align: center;
  margin: 0 5px 5px 0;
  background: #FFF;
  color: #333;
  border-radius: 3px;
  box-sizing: border-box;
}
label:hover {
  border-color: #ADADAD;
  background: #EBEBEB;
  cursor: pointer;
}
input[type=checkbox]:checked + label {
  order: 0;
  background-color: #5cb85c;
  border-color: #4cae4c;
  color: #FFF;
}
input[type=checkbox]:checked + label:hover {
  background-color: #47a447;
  border-color: #398439;
}

这样看起来还有上升空间,如果加上几个图标响应用户操作,那么学习成本会更低,对操作后的预期也会更准确。于是引用CDN上的font-awesome,使用:before伪类加上小图标,就得到了最终效果:

我无意中发现,这样批量添加删除时,鼠标可以常点不动,应该也是个意外的收获吧。

第三步,加入动画教育用户(失败)

至此功能基本做好了,不过由于修改了行为,可能导致用户迷惑,所以准备加个动画帮助用户理解这个交互。

可惜作为一个新功能,浏览器的支持尚不完善,虽然规范中规定“animatable: yes”,但是实测在Chrome v.30也无法工作:http://jsfiddle.net/meathill/Ka66W/1/

看来只有等新版浏览器发布后再去完善了。

兼容性

使用纯CSS做组件,几乎不用担心兼容性问题,因为浏览器本身就做了很好的向下兼容,代码最多不生效,一般不会错。

具体到这个组件,因为只针对视觉效果,没有增删改任何浏览器行为,所以兼容性也没有任何问题。不过最终效果呢,只有支持flexbox和CSS3选择符的浏览器才能正常渲染。

我的环境是Window 8 + Chrome v.30,以及小米2 + Chrome v.30,测试通过。

后记

如今CSS很强,纯CSS可以实现很多功能,希望今后能做出更多有价值的东西。分享这个组件的实现,希望对大家有用。