lio-mengxiang/mx-design

我的天!又在几个知名组件库发现相同的bug!(组件库zIndex管理方案)

Closed this issue · 0 comments

前言

这篇文章的重点是z-index管理方案,主标题是标题党吸引点流量,请谅解,希望我用高质量的方案对比来消解你对标题党的怒火😅。

有些人可能觉得z-index有啥难的,这其实是一个很经典的前端难题了。我们先看看以下几个组件库如何让它们的z-index管理出现异常。

以下问题在国内3个知名组件库,阿里的ant design,腾讯的tdesign和semi-design出现。后续也会简单说一下他们的z-index管理方案的原理以及出现问题的原因。

这里字节的arco design是解决了这个问题的。后面也会讲arco是怎么设计的。

我们先看如何复现问题,有在线案例。

首先我们把一个两个Button组件放在一起,如下图:

在线链接

image.png

然后第一个Button组件是触发弹出Modal框的,第二个按钮是类似Tooltip组件,我们让这个弹层永远显示,这样好复现bug。

然后点击Open Modal的button按钮,出现Tooltip把Modal的黑色蒙层遮盖了的问题。

image.png

你可能说这算啥bug,出现几率很低,我们再来一个?

看看ant vue有没有问题(只要你知道他的z-index方案,你能想出n个方法让他出问题)

在线链接

image.png

在分析产生问题的原因前,大家是否想过一个问题,你可以看看你用的组件库,把一些弹出框组件(例如,modal组件,tooltip组件,message组件等等)都渲染在了dom流的哪个地方?

答案是body下,如下图

image.png

你思考过为什么要这么做吗?比如上图,正常情况,不是应该按钮在哪里,这个对应的弹框跟按钮在一起吗,渲染到body下干嘛?

这其中一个重要的原因就是为了管理z-index。

层叠上下文

为了说明这个问题,我们还要弄清楚一个概念,叫层叠上下文?我想问大家,zIndex越大一定就在最高的层级吗?

答案是no!

举个例子

<style>
    .box1,
    .box2 {
        position: relative;
        z-index: 1;
    }

    .child1 {
        width: 200px;
        height: 100px;
        background: #168bf5;
        text-align: right;
        position: absolute;
        top:0;
        z-index: 99999;
    }

    .child2 {
        width: 100px;
        height: 200px;
        background: #32c292;
        position: absolute;
        top:0;
        z-index: 1;
    }
</style>
</head>

<body>
    <div class="box1">
        <div class="child1">child1</div>
    </div>

    <div class="box2">
        <div class="child2">child2</div>
    </div>
</body>

以上代码,我们可以看到child2,zIndex是1,child1的zIndex是99999,按道理来说,child1的zindex更大,它应该展示在child2上面,可是结果如下:

image.png

原因就是box1和box2都创造了层叠上下文(如果有zindex为数字 + 非postion:static布局会产生层叠上下文,还有很多条件也能创造层叠上下文,这里就不细说了)

box1和box2的层叠等级一样,所以遵循谁后写谁在上面,所以box2永远在box1上面

所以box2里面的元素,是永远比box1里面的元素层级更高的。

那么child1和child2比较根本没有意义,因为他们并不在一个层叠上下文中,只有在一个层叠上下文中,比较zindex才有意义。

为什么放到body下

因为我们可以看到,业务代码有可能会在很多隐蔽的地方产生层叠上下文,这个组件库是无法控制的,所以如果大家把很可能产生遮盖效果异常的组件都放在body上,就相当于大家在一个层叠上下文中了,可以更好的控制遮盖问题。

特殊情况

从上面来看,放在body下,大家都在一个层叠上下文中,那么就会遵循谁后出现,谁在层级之上的效果,但是总有一些常见的情况是不想要这样的,比如:

  • 我有一个message组件,弹出消息,3秒之后消失,在1秒的时候我就点击modal框,但是我们遵循谁后点击,谁在层级上,那么modal组件就把message组件覆盖了,这并不是我们想要的。

所以一般情况,对于message和notification的弹出消息,我们总是希望他们层级是最高的。

  • 还有就是我在文章开始复现的一个问题,就是因为modal的z-index没有tooltip的层级高

这下大家知道为什么产生问题的原因了吧。如何解决呢?我们看看bootstrap5的方案:

bootstrap zindex设计

$zindex-dropdown:                   1000;
$zindex-sticky:                     1020;
$zindex-fixed:                      1030;
$zindex-modal-backdrop:             1040;
$zindex-offcanvas:                  1050;
$zindex-modal:                      1060;
$zindex-popover:                    1070;
$zindex-tooltip:                    1080;

这个简单看看就好,我觉得有点过时了,因为bootstrap是在jquery那个年代的流行产物,并不会将所有弹出框类似的组件渲染到body下.

但是这起码说明一个问题,就是按照bootstrap这个标准,至少很少有弹出层异常的问题,为什么是很少有呢?

因为特殊情况基本上都是层叠上下文导致的,这种特殊情况只有组件单独导入zindex适配业务需求。

通过设置 z-index层级的方案

类似bootstrap,通过对特殊弹框类组件设置不同的z-index来避免遮盖问题,我们列举了以下方案:

ant design

// ant-design/components/style/themes/default.less
/* z-index列表, 按值从小到大排列 */
@zindex-badge: auto;
@zindex-table-fixed: 2;
@zindex-affix: 10;
@zindex-back-top: 10;
@zindex-picker-panel: 10;
@zindex-popup-close: 10;
@zindex-modal: 1000;
@zindex-modal-mask: 1000;
@zindex-message: 1010;
@zindex-notification: 1010;
@zindex-popover: 1030;
@zindex-dropdown: 1050;
@zindex-picker: 1050;
@zindex-popoconfirm: 1060;
@zindex-tooltip: 1070;
@zindex-image: 1080;

在我的组件库里,因为popover,dropdown,tooltip,selelct类型的下拉框都属于popup组件,所以跟ant design略有不同,他们都是一个层级。

为什么我能试出来ant vue的bug,大家可以看ant design中@zindex-dropdown: 1050,然后@zindex-popover: 1030,那么意味着,在同一个层叠上下文中,我先触发dropdown,再触发popover,那么popover一定是在dropdown底下的,所以会产生bug。

后来我看ant design5学聪明了,不支持传入组件,只能传入数组了。。。我的组件也是这么做滴,嘿嘿,当然不仅仅是因为这个bug,后期要为低代码平台做铺垫,所有传入的数据最好都是普通数据,比如数组,对象,而不是react组件。

全局管理器方案

elementUI将弹窗层级管理收敛到了一个入口PopupManager中,涉及zIndex层级的弹窗组件实例都需要注册到PopupManager中。

简单来说,就是用一个全局的对象记录当前最高的zindex,然后下一个比这个更高,简单来说如下:

class ZIndexManager {
  constructor() {
    this.zIndex = 1000; // 初始的 z-index 值
    this.zIndexMap = new Map(); // 用于存储元素和对应的 z-index 值
  }

  getNextZIndex() {
    this.zIndex += 1;
    return this.zIndex;
  }

  registerElement(element) {
    const nextZIndex = this.getNextZIndex();
    this.zIndexMap.set(element, nextZIndex);
    this.updateElementZIndex(element, nextZIndex);
  }

  unregisterElement(element) {
    this.zIndexMap.delete(element);
  }

  updateElementZIndex(element, zIndex) {
    element.style.zIndex = zIndex;
  }
}

const zIndexManager = new ZIndexManager();
export default zIndexManager;

但是我觉得,很多场景并不是说我需要后面出现的弹层一定要比前面的层级高。

我们之前也说了,message(也就是toaster)肯定是最高层级的,我们不希望modal比它还高,所以这个方案我觉得还能更好。

改进ant design方案

在我看来,ant deisgn的方案稍微改一下,基本上就使用百分之95%以上的场景了,特殊场景用户自己去单独给组件传入z-index或者改变层叠上下文的层级,也就是自定义设置了。

以下层级由低到高:

  • Affix
  • Drawer, Message, Modal,modal-mask, popup相同层级(从而让后出来的在层级最上面)
  • notification
  • message

上面的popup包含很多,比如select所有的类似下拉框组件(比如picker),tooltip,dropdown等等

arco design方案

字节的arco design在这方面我觉得是国内做的比较好的,文章初的两种bug均对它无效。

字节的处理基本上跟我上面改进的方案差不多,但是它只对Modal和Drawer组件内部的所有组件的z-index进行了+1处理

为什么要这么做,我们要看下arco的z-index方案。

  // z-index
  '--z-index-popup-base': 1000,
  '--z-index-affix': 'calc(var(--z-index-popup-base) - 1)', // 999
  '--z-index-popup': 'var(--z-index-popup-base)', // 1000
  '--z-index-drawer': 'calc(var(--z-index-popup-base) + 1)', // 1001
  '--z-index-modal': 'calc(var(--z-index-popup-base) + 1)', // 1001
  '--z-index-tooltip': 'var(--z-index-popup-base)', // 1000
  '--z-index-message': 'calc(var(--z-index-popup-base) + 3)', // 1003
  '--z-index-notification': 'calc(var(--z-index-popup-base) + 3)', // 1003
  '--z-index-image-preview': 'calc(var(--z-index-popup-base) + 1);', // 1001

以上是我的最开始的z-index方案,就是从arco借鉴而来,但是我们发现上面有什么问题呢?modal的zindex是1001,popup的zindexshi 1000,意味着我先打开modal框,然后modal框里有一个popup按钮,再触发popup按钮后,显示的文字居然回到modal框后面(我的组件库目前有这个bug)

所以acro怎么避免这个情况呢,例如,在modal框里,会把modal框里传入的组件所有index重新设置为当前modal的zindex + 1,所以arco避免了这种bug。

而我怎么做呢,我只要把modal的z-index改成和popup一样,这不就天然是谁后出现,谁在上面了吗,巧妙的达成了和arco一样的效果。

求个star

做组件库教程不易,求个star,哈哈,