element-ui 下拉组件弹窗位置分离问题解决

问题背景:

团队最近在做低代码平台的过程中,发现用户使用低代码构建的弹窗页面里面,下拉框及日期选择组件的弹出框都在页面滚动以后出现了位置偏离:


image.png

问题调查:

首先猜测如果要解决这个问题,应该监听滚动条滚动事件,在事件回调中触发弹框位置的更新,使其跟随input框移动;

看源码验证猜想:
在element-ui/src/utils/popper.js中,453行有事件绑定的方法:

    /**
    监听滚动事件从而改变popper的位置
     * Setup needed event listeners used to update the popper position
     * @method
     * @memberof Popper
     * @access private
     */
    Popper.prototype._setupEventListeners = function() {
        // NOTE: 1 DOM access here
        this.state.updateBound = this.update.bind(this);
        root.addEventListener('resize', this.state.updateBound);
        // if the boundariesElement is window we don't need to listen for the scroll event
        if (this._options.boundariesElement !== 'window') {
        //   非常重要,就在这里,他要去找滚动元素
            var target = getScrollParent(this._reference);
            // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
            if (target === root.document.body || target === root.document.documentElement) {
                target = root;
            }
            target.addEventListener('scroll', this.state.updateBound);
            this.state.scrollTarget = target;
        }
    };

接下来看,getScrollParent 写了什么:

    /**
     * Returns the scrolling parent of the given element
     * @function
     * @ignore
     * @argument {Element} element
     * @returns {Element} offset parent
     */
    function getScrollParent(element) {
        var parent = element.parentNode;

        if (!parent) {
            return element;
        }

        if (parent === root.document) {
            // Firefox puts the scrollTOp value on `documentElement` instead of `body`, we then check which of them is
            // greater than 0 and return the proper element
            if (root.document.body.scrollTop || root.document.body.scrollLeft) {
                return root.document.body;
            } else {
                return root.document.documentElement;
            }
        }

        // Firefox want us to check `-x` and `-y` variations as well
        if (
            ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow')) !== -1 ||
            ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-x')) !== -1 ||
            ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-y')) !== -1
        ) {
        //   重要: 递归往上查找,一旦找到含有overflow: scroll/auto/overflow-x/overflow-y的元素,立刻返回这个元素,用于绑定scroll事件
            // If the detected scrollParent is body, we perform an additional check on its parentNode
            // in this way we'll get body if the browser is Chrome-ish, or documentElement otherwise
            // fixes issue #65
            return parent;
        }
        return getScrollParent(element.parentNode);
    }

可以得出结论: popper.js源码里面,会根据input元素作为reference元素递归往上查找,一旦找到含有overflow: scroll/auto/overflow-x/overflow-y的元素,立刻返回这个元素,用于绑定scroll事件;然而,如果绑定scroll的元素不是肉眼可见的有滚动条滚动的元素,那么即使滚动了滚动条,也不会触发这个scroll事件;

解决措施:

办法一: 调整布局,不产生多余带有overflow: scroll/auto/overflow-x/overflow-y的元素,保证肉眼可见有滚动条的元素,就是会被绑定scroll事件的元素;
办法二: 修改popper.js源码,即使找到了第一个带有overflow:auto的父级元素,依然往上查找还带有overflow:auto的元素,找到以后,也给绑定相同的scroll事件,可以递归往上查找。此处做一个往上再查找一级的例子:

  Popper.prototype._setupEventListeners = function() {
    // NOTE: 1 DOM access here
    this.state.updateBound = this.update.bind(this);
    root.addEventListener('resize', this.state.updateBound, true);
    // if the boundariesElement is window we don't need to listen for the scroll event
    if (this._options.boundariesElement !== 'window') {
      var target = getScrollParent(this._reference);
      let parentTarget = null;
      // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
      if (target === root.document.body || target === root.document.documentElement) {
        target = root;
      } else {// =====再找一级====
        parentTarget = getScrollParent(target);
      }
      // debugger;
      target.addEventListener('scroll', this.state.updateBound, true);
      if (parentTarget) {
        // 绑定同样的监听事件
        parentTarget.addEventListener('scroll', this.state.updateBound, true);
      }
      this.state.scrollTarget = target;
      this.state.parentTarget = parentTarget;
    }
  };

暂时只想到这俩办法;当然在办法二的基础上,如果直接去改element源码成本较高,因为需要各个组件重新修改依赖;建议考虑覆盖原型方法去做,这样不用去手动修改依赖;

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容