让我们再深入谈谈用户界面(UI)方面的问题。在上一章中,我们解决了恼人的 “闪烁” 问题。现在,让我们来看看另一个有趣的 UI 漏洞:内容裁剪。
你可能听说过,在 React 中,当在设置了overflow: hidden的元素内部渲染元素时,我们需要使用 Portals(传送门)来解决这个问题。网上每两篇关于 Portals 的文章中就有一篇会提到这个例子。但实际上这并不正确:我们仅用纯 CSS 就能解决内容 “裁剪” 问题。我们使用 Portals 是出于其他原因。这个 “溢出问题” 还可能会给人一种错误的安全感:如果应用中没有任何overflow: hidden的设置,我们就可以轻松且安全地在任何位置放置任何元素。这同样不正确。
让我们深入探讨这个问题,并学习:
- 元素的CSS定位如何运转。
- 什么是栈上下文。
- 如何用CSS来避免内容裁剪。
- 为什么我们不能只用CSS来解决这个问题,还要用Portals。
- Protals是如何运转的以及其注意事项。
事先什么,这篇文章有很多CSS的内容。前半部分会讨论CSS的内容,因为很多React开发者对CSS并不是很熟练。
CSS: 绝对定位
让我们从一个简单的app切入。
我们有一个页面,页面中间有一个按钮。当我们点击按钮时,我想展示一些额外信息。
const App = () => {
const [isVisible, setIsVisible] = useState(false);
return (
<>
<SomeComponent />
<button onClick={() => setIsVisible(true)}>
show more
</button>
{isVisible && <ModalDialog />}
<AnotherComponent />
</>
);
};
为了实现这个功能,当额外内容出现时,它会把AnotherComponet给往下推。这是普通的HTML文档流,也是像div,p等块状标签的默认行为。
但是,如果我们想把额外内容以对话框的形式展示,该怎么办?我希望的的是ModalDialog能够脱离这个普通文档流。实现这一效果最常见的方法,就是使用CSS的定位属性。
定位属性中,有两个值可以帮助属性脱离普通文档流:absolute和fixed。让我们从absolute说起。我们需要做的,是把这个属性加到ModalDialog组件里。
// somewhere where you declare your css
.modal {
position: absolute;
}
// our React component
const ModalDialog = () => {
return (
<div className="modal">
some additional info
</div>
)
}
瞧!内容不再是文档布局的一部分,并出现在顶部。现在我只需要通过在 CSS 的 top 和 left 属性中设置一些有意义的值,将其正确定位。假设我想把对话框放在屏幕中间,它的 CSS 代码大概是这样的:
.modal {
position: absolute;
width: 300px;
top: 100px;
left: 50%;
margin-left: -150px;
}
这个对话框会出现在屏幕中间!
代码示例: advanced-react.com/examples/13…
技术层面而言,这是可行的。但是如果你检视现存对话框的CSS,非常有可能它们用的并不是绝对定位。
下面是原因。
绝对定位 并不是 那么 绝对
首先,绝对定位并没有那么绝对。绝对定位其实是相对的,相对于最近的一个有position属性的元素。在刚刚的例子中,绝对定位生效仅仅是一个巧合:因为在对话框和id为app的div之间,我并没有为其他元素设置position属性。
如果这个对话框渲染在一个position为relative的div之间(值为sitcky或absolute亦然),且这个div并不居于页面中间。那么,这个对话框只会出现在这个div的中间,而不是整个视图的中间。
代码示例: advanced-react.com/examples/13…
所以,对于要渲染在屏幕正中央的元素,使用postion属性并不是最好的方案。当然,虽然通过计算仍然可以实现,但仅靠纯 CSS 是做不到的。
但是,对于提示框、下拉菜单这样的场景呢?这些元素只要相对于其父级有绝对定位即可,对吧?是的,绝对定位对于这种场景是最佳选择,再辅之以offsetLeft 和 offsetTop.
从技术上讲,是的,这是可行的。除非堆叠上下文(Stacking Context)规则起作用。
理解堆叠上下文(Stacking Context)
对于给有position属性的元素添加z-index属性的人来说,叠上下文(Stacking Context)不啻于是一个噩梦。叠上下文(Stacking Context)是以3d 视觉审视HTML元素的一个视角。它就好比Z坐标轴,(width 和 height 好比X轴和Y轴),Z 轴用于定义元素之间的堆叠顺序,即哪个元素位于其他元素的上方。比如说,如果一个元素有shadow属性,且与周围属性发生重叠了,它是该渲染在周围元素之上,还是周围元素之下呢?这是由堆叠上下文(Stacking Context)决定的。
叠上下文(Stacking Context)的默认规则是很复杂的。
通常情况下,元素按照它们在文档对象模型(DOM)中出现的顺序进行堆叠。就像在这样的代码中:
<div>grey</div>
<div>red</div>
<div>green</div>
在叠上下文(Stacking Context)的视角,绿div在红div之前,红div又在灰div之前。如果我给这些div添加负边距,它们会变成这样:
position值为absoulte或者relatvie的元素将会被推到最前面。如果我把红div的position设置为relative,绿div将位于其下。
<div>grey</div>
<div style={{ position: "relative" }}>red</div>
<div>green</div>
如果我们把对话框放在红div里面,它将位于最上层、屏幕最中间。如果它被放在了灰div里面,红div依然在对话框的上方。
为了解决这个问题,我们可以使用z-index属性。这个属性允许我们操作同一个叠上下文(Stacking Context内元素在Z轴的位置。所以,如果我把z-index设置为负数,这个元素会被渲染在底部。如果把z-index设置为正数,这个元素会被渲染在顶部。
这里的关键在于 “在同一堆叠上下文内”。如果某个元素创建了一个新的堆叠上下文,那么该元素的z - index将相对于这个新的上下文。这就像是一个完全独立的 “气泡”。新的堆叠上下文会像一个独立的黑箱,依据父上下文的规则进行控制,其内部发生的情况仅在内部生效。
在同一元素上同时使用 position(如 position: relative 或 position: absolute 等) 和 z - index 会创建其自身的堆叠上下文。从我们彩色 div 的角度来看,这意味着如果我给灰色 div 添加 position: relative 且 z - index: 1,给红色 div 添加 position: relative 且 z - index: 2,那么它们两个都将成为各自堆叠上下文的父元素。灰色 div 及其内部的所有元素都将位于红色 div “下方”,包括我们的模态对话框。即使我把对话框的 z - index 改为一个很大的神奇数字,也无济于事:对话框仍会显示在红色 div 下方。
在下面的代码示例中,试着调整灰色 div 的(这里原文缺失相关属性,推测是 z-index 之类的)属性,真的很有意思。如果我把这个属性移除,新的堆叠上下文就会消失,此时对话框就会遵循全局上下文及其规则,开始显示在红色 div 的上方。一旦我给灰色 div 添加一个比红色 div 的(同样推测缺失的属性为 z-index)值小的该属性值,灰色 div 及其内部元素就会移到红色 div 下方。
顺便说一下,触发堆叠上下文的不只是position 和 z-index)的组合,transform属性单独也会有这样的效果。所以,你代码里残留的任何 CSS 动画都有可能打乱已定位元素的显示布局。Flex 布局或 Grid 布局里的子元素也会出现这种问题。此外,还有很多其他不同的属性也会导致类似情况。
代码示例: advanced-react.com/examples/13…
当然,最后要说的是带有 overflow 属性的元素。顺便提一下,仅仅给一个元素设置 overflow 属性并不会裁剪其内部绝对定位的 div;它需要和 position: relative 一起使用才行。不过确实,如果一个绝对定位的对话框在同时设置了 overflow 和 position 属性的 div 内部渲染,那么它就会被裁剪。
代码示例: advanced-react.com/examples/13…
针对所有这些问题我们能做点什么吗?当然可以,不过只能部分解决。至少,我们能迅速解决溢出问题。