🤖
AI审核中

手机上被撑破的搜索弹层:网格 auto 列、两行截断与出屏的底边

文章摘要

给博客搜索弹层加了热门标签和最近文章,电脑上排得整齐,手机上却被撑出了弹层。记录三个坑:网格 auto 列不会比不换行的内容窄、两行截断时内边距漏出第三行半截、固定弹层在小屏上底边出屏,以及之后在多个尺寸下量越界的办法。

10 月 8 日给博客的搜索弹层加了两组建议:热门标签和最近 5 篇文章,打开搜索不用输入就能直接点。我在 1440 宽的电脑上截了图,排得整整齐齐,就发版了。

没过多久收到一张手机截图:标签那一行最右边的「面试」只露出半个字,最近文章的标题直接伸出了弹层的右边框,「后台的『分享签名校验』」整段被切掉。

问题不难修,难堪的是它本来一眼就能看到——我根本没在手机尺寸下打开过这个弹层。这篇把这次踩到的三个坑记下来,最后附上之后用来量这类问题的办法。

现象:电脑上正常,手机上越界

先用脚本量一遍线上版本:打开弹层,数一数弹层里有多少个元素的边框超出了弹层本身。

视口 弹层宽 越界元素
360×780 320px 20 个
320×568 280px 20 个
390×664 350px 19 个
1440×900 580px 0 个

电脑上一个都没有,手机上全军覆没。最长的那篇标题单行排开约 518px:比电脑上的 580px 弹层窄,比手机上的任何一个都宽。所以这个问题在电脑上根本不会出现。

一、网格的 auto 列,不会比内容窄

建议区是这样写的:

.search-suggest { display: grid; gap: 14px; }
.search-suggest-posts { display: grid; gap: 2px; }
.search-suggest-posts a {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

本意是标题太长就在末尾显示省略号。可省略号一个都没出现,标题反而把整块撑了出去。

原因在 display: grid 没写 grid-template-columns。这时网格只有一条隐式的列,宽度是 auto。auto 列有个下限:不能比里面内容的「最小内容宽度」更窄。而一段 nowrap 的文字不能在任何地方断开,它的最小内容宽度就是整行的长度。

于是标题有多长,这一列就有多宽。链接自己的 overflow: hidden 根本没机会生效:它拿到的宽度本来就放得下整行,没有什么可截的。

用一个 300px 宽的容器单独复现,量到的数字是这样的:

写法 列宽 链接宽
默认 auto 列 518px 518px
只改外层 300px 518px
两层都改 300px 300px

第二行是我修第一遍时踩的:外层网格改成 minmax(0, 1fr) 之后,外层的列确实缩回了 300px。可最近文章的列表 ul 自己也是一个网格,它的列还是 auto,链接照样是 518px。线上量出来的越界元素从 20 个降到 10 个,没有归零。

两层都改过之后,列宽回到 300px,省略号也出来了:

.search-suggest {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
}
.search-suggest-posts {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
}
.search-suggest-posts li { min-width: 0; }

minmax(0, 1fr) 的意思是:这一列最小可以是 0,最大占满剩余空间。下限从「内容的最小宽度」改成了 0,列就会老老实实服从容器的宽度。

标签那一行被切掉也是同一个原因。标签是 flex-wrap: wrap,按容器宽度换行,可它的容器已经被标题撑到了 518px,标签就排到 518px 才换行。手机上看,就是最右边那个标签被弹层的边框切掉。

flex 布局里也有同样的规则:flex 子项默认 min-width: auto,同样不会比内容窄。碰到不换行的长文字、长网址、长代码,要么给子项写 min-width: 0,要么给列写 minmax(0, 1fr)。

二、两行截断,漏出了第三行的半截

列宽对了,省略号也出来了,可手机上 5 篇标题被截掉 4 篇,每篇只剩一行,长标题的后半句全看不到。

视口 单行省略 两行截断
360×780 截 4 篇 截 1 篇
320×568 截 5 篇 截 2 篇
390×664 截 4 篇 截 0 篇

于是改成最多显示两行:

.search-suggest-posts a {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 7px 10px;
}

截图一看,两行之下还能看到第三行文字的上半截,像被人用尺子沿着字腰划了一刀。(站里另一篇《基于 -webkit-line-clamp 的评论折叠组件开发》写过怎么用这个属性做折叠,这次碰到的是它和内边距之间的关系。)

-webkit-line-clamp 只负责在第二行末尾加省略号,后面的文字照样排版,只是被 overflow: hidden 裁掉。而 overflow 是按内边距的外缘裁的:内边距本身也算可见区域。

按 20px 行高算:

  • 两行文字占 40px,上下内边距各 7px,元素的可见高度是 54px;
  • 第三行从第 47px(7 + 40)开始,到第 54px 被裁掉,正好露出 7px,差不多是半个字。

量到的也是这样:可见高度 54px,内容总高 74px。

解决办法是把上下的留白从内边距换成透明边框:

.search-suggest-posts a {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 0 10px;
    border: 7px solid transparent;
    border-left: 0;
    border-right: 0;
}

边框不属于裁切区域,可见高度变成正好 40px,第三行一个像素都不露。背景默认会铺到边框底下,所以鼠标悬停时那块浅色背景的大小和原来一样。

三、固定的弹层,底边出了屏幕

两行截断让每篇标题高了一截,在 iPhone SE 这种 320×568 的小屏上,弹层从第 102px 开始,到第 681px 才结束,比屏幕底部多出 113px。

如果只是长一点,往下滑就行。可这个弹层是固定定位的,打开时页面又被锁住了滚动(body 设成了 position: fixed),整个屏幕没有任何地方能滚。最后两篇文章就在屏幕下面,怎么都点不到。

做法是让弹层自己限高,超出的部分在弹层里面滚:

.search-dialog {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 120px);
    max-height: calc(100dvh - 120px);
}
.search-input-area { flex: none; }
.search-suggest {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}

几个细节:

  • 100dvh 是去掉浏览器地址栏之后的可见高度,不认识它的旧浏览器会用上面那行 100vh;
  • 输入框不参与收缩,保证搜索框始终在;
  • min-height: 0 又是第一节那条规则:flex 子项默认 min-height: auto,不会比内容矮,不写这一句,overflow-y: auto 永远等不到需要滚动的那一天;
  • overscroll-behavior: contain 让滑到头时不会带着后面的页面一起动。

改完再量:

视口 改前底边 改后底边
320×568 681px 550px
390×400 594px 352px

390×400 模拟的是键盘弹起后只剩一半屏幕的情况。

最后用触摸手势在建议区里往上划了 300px:建议区滚动了 130px,最后一篇完整露出来,背后的页面一动没动。

之后怎么测

这次的根本问题不在 CSS,而在于没测。之后凡是改了弹层、网格、不换行文字、固定定位的地方,发版前都按这个办法在几个尺寸下各量一遍:

// 打开弹层之后在页面里执行:弹层里有哪些元素越过了弹层的左右边界
const dialog = document.querySelector('#search-overlay .search-dialog');
const box = dialog.getBoundingClientRect();
const outside = [...dialog.querySelectorAll('*')].filter(function (el) {
    const r = el.getBoundingClientRect();
    return r.width > 0 && (r.right > box.right + 0.5 || r.left < box.left - 0.5);
});
const result = {
    outside: outside.length,
    pageOverflow: document.documentElement.scrollWidth > innerWidth,
    bottomOut: box.bottom > innerHeight
};
  • 尺寸至少覆盖 360×780、320×568、390×664 三个手机尺寸和一个电脑尺寸,浅色和深色各一遍;
  • 三个数都要是 0、false、false,再截图人工看一眼;
  • 要滚动的地方用触摸手势真的划一下(Chrome 开发者协议里的 Input.synthesizeScrollGesture),看滚的是不是该滚的那个元素。

小结

  • 网格的 auto 列和 flex 子项默认都不会比内容的最小宽度更窄,碰到不换行的长文字,写 minmax(0, 1fr) 或 min-width: 0;嵌套的每一层都要检查。
  • overflow: hidden 按内边距的外缘裁切,多行截断要留白时用透明边框,别用上下内边距。
  • 固定定位的弹层要自己限高、自己滚,flex 子项记得写 min-height: 0。
  • 「在电脑上看过」不等于「测过」。
0 条评论

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧

评论区 0

头像
 0 条评论