问题背景:
团队最近在做低代码平台的过程中,发现用户使用低代码构建的弹窗页面里面,下拉框及日期选择组件的弹出框都在页面滚动以后出现了位置偏离:
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源码成本较高,因为需要各个组件重新修改依赖;建议考虑覆盖原型方法去做,这样不用去手动修改依赖;