Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
lio-mengxiang opened this issue Aug 25, 2023 · 0 comments
Labels
documentation Improvements or additions to documentation

Comments

@lio-mengxiang
Copy link
Owner

前言

这篇文章的重点是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,哈哈,

@lio-mengxiang lio-mengxiang added the documentation Improvements or additions to documentation label Sep 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant