首页 > 解决方案 > 如果父组件位于滚动容器内,则解决 Vuetify “v-menu”显示为固定

问题描述

Vuetify中的组件存在一个长期存在的问题:v-menu

  1. 默认情况下,弹出窗口与激活器物理“分离”并创建为 的子级v-app,从而避免在某些父 DOM 节点具有overflow: hidden样式时被剪裁;但是,这会导致当激活器位于滚动容器内时弹出窗口表现为“位置:固定”的问题 - 也就是说,它不会随激活器滚动并且看起来在视觉上是断开的,只是“悬挂”在页面上。
  2. Vuetify 维护人员承认这一事实并建议使用“attach”道具 - 但是,在使用“attach”时,10 次中有 9 次计算错误的弹出窗口的位置。

经过 2 小时的调试,我终于放弃了使用“attach”属性,决定简单地跟踪激活器所在的父容器的滚动位置,并在计算弹出窗口的位置时将其考虑在内。我正在分享我对以下问题的解决方案,并希望它能够包含在主流的 Vuetify 中。

标签: scrollmenuvuetify.jsfixed

解决方案


这是解决上述问题的补丁文件以及其他一些问题。在您的项目中创建一个名为的文件夹patches并将补丁文件保存为patches/vuetify#2.5.5.patch. scripts然后在您的组中添加一个新脚本package.json

"scripts":
{
   ....
   "prepare": "custompatch"
}

然后运行npm -i -D custompatch(为您的 CI/CD 安装修补程序)和npx custompatch(在您的开发环境中实际修补 Vuetify)。

Index: \vuetify\lib\components\VMenu\VMenu.js
===================================================================
--- \vuetify\lib\components\VMenu\VMenu.js
+++ \vuetify\lib\components\VMenu\VMenu.js
@@ -124,10 +124,10 @@
       return {
         maxHeight: this.calculatedMaxHeight,
         minWidth: this.calculatedMinWidth,
         maxWidth: this.calculatedMaxWidth,
-        top: this.calculatedTop,
-        left: this.calculatedLeft,
+        top: `calc(${this.calculatedTop} - ${this.scrollY}px + ${this.originalScrollY}px)`, // we deduct the difference to account
+        left: `calc(${this.calculatedLeft} - ${this.scrollX}px + ${this.originalScrollX}px)`, // for the change in scroll
         transformOrigin: this.origin,
         zIndex: this.zIndex || this.activeZIndex
       };
     }
Index: \vuetify\lib\components\VTextField\VTextField.js
===================================================================
--- \vuetify\lib\components\VTextField\VTextField.js
+++ \vuetify\lib\components\VTextField\VTextField.js
@@ -433,8 +433,9 @@
       this.$refs.input.focus();
     },
 
     onFocus(e) {
+      this.onResize(); // this fixes the wrong position of the label when the input is focused - label is off by 8-10 pixels to the right, overlapping the field border
       if (!this.$refs.input) return;
       const root = attachedRoot(this.$el);
       if (!root) return;
 
Index: \vuetify\lib\directives\click-outside\index.js
===================================================================
--- \vuetify\lib\directives\click-outside\index.js
+++ \vuetify\lib\directives\click-outside\index.js
@@ -35,10 +35,11 @@
 }
 
 function directive(e, el, binding) {
   const handler = typeof binding.value === 'function' ? binding.value : binding.value.handler;
+  const target = e.target;
   el._clickOutside.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
-    checkIsActive(e, binding) && handler && handler(e);
+    checkIsActive({...e, target}, binding) && handler && handler({...e, target});  // this fixes a strange behavior - e.target on this line differs from e.target outside of the closure when Vuetify is inside a Shadow DOM
   }, 0);
 }
 
 function handleShadow(el, callback) {
Index: \vuetify\lib\mixins\detachable\index.js
===================================================================
--- \vuetify\lib\mixins\detachable\index.js
+++ \vuetify\lib\mixins\detachable\index.js
@@ -22,13 +22,23 @@
     },
     contentClass: {
       type: String,
       default: ''
+    },
+    scroller:
+    {
+      default: null, // it works the same way as "attach" - but must refer to the scrolling parent of the activator
+      validator: validateAttachTarget
     }
   },
   data: () => ({
     activatorNode: null,
-    hasDetached: false
+    hasDetached: false,
+    scrollingNode: null,
+    scrollX: 0,
+    scrollY: 0,
+    originalScrollX: 0,
+    originalScrollY: 0
   }),
   watch: {
     attach() {
       this.hasDetached = false;
@@ -36,10 +46,38 @@
     },
 
     hasContent() {
       this.$nextTick(this.initDetach);
+    },
+    isActive(val)
+    {
+      if (val)
+      {
+        if (typeof this.scroller === 'string') {
+          // CSS selector
+          this.scrollingNode = document.querySelector(this.scroller);
+        } else if (this.scroller && typeof this.scroller === 'object') {
+          // DOM Element
+          this.scrollingNode = this.scroller;
+        }
+        if (this.scrollingNode)
+        {
+          this.originalScrollX = this.scrollingNode.scrollLeft; // we only need the difference between scrolling position
+          this.originalScrollY = this.scrollingNode.scrollTop; // before opening the menu and scrolling position while the menu is open
+          this.scrollX = this.originalScrollX; // current scrolling position will be updated by the event handler
+          this.scrollY = this.originalScrollY;
+          this.scrollingNode.addEventListener('scroll', this.setScrollOffset, {passive: true});
+        }
+      }
+      else
+      {
+        if (this.scrollingNode)
+        {
+          this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+        }
+        this.scrollingNode = null;
+      }
     }
-
   },
 
   beforeMount() {
     this.$nextTick(() => {
@@ -79,9 +117,12 @@
     } catch (e) {
       console.log(e);
     }
     /* eslint-disable-line no-console */
-
+    if (this.scrollingNode)
+    {
+      this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+    }        
   },
 
   methods: {
     getScopeIdAttrs() {
@@ -117,9 +158,13 @@
       }
 
       target.appendChild(this.$refs.content);
       this.hasDetached = true;
+    },
+    setScrollOffset(event)
+    {
+      this.scrollX = event.target.scrollLeft;
+      this.scrollY = event.target.scrollTop;
     }
-
   }
 });
 //# sourceMappingURL=index.js.map
\ No newline at end of file 

推荐阅读