tooltip-position-solution

Element Plus Tooltip 在滚动容器中位置超出问题解决方案

一、问题描述

在树组件(el-treeel-tree-select)中使用 el-tooltip 显示节点提示信息时,当容器支持横向滚动时,滚动条拖动后 tooltip 提示框会超出容器边界,无法始终保持在容器内部显示。

二、问题原因

  1. el-tooltip 默认基于触发元素定位,不会自动感知滚动容器的边界
  2. 当触发元素被滚动到容器边缘时,tooltip 仍然按照原始位置计算,导致超出容器

三、解决方案

核心技术:popper-options 配置边界约束

使用 Popper.js 的 preventOverflowflip 修饰符,将 tooltip 的边界约束在指定容器内。

关键代码

javascript 复制代码
const containerRef = ref<HTMLElement | null>(null)

const tooltipPopperOptions = computed(() => ({
  modifiers: [
    {
      name: 'preventOverflow',
      options: {
        boundary: containerRef.value || undefined,
      },
    },
    {
      name: 'flip',
      options: {
        boundary: containerRef.value || undefined,
      },
    },
  ],
}))
vue 复制代码
<el-tooltip :popper-options="tooltipPopperOptions" placement="top">
  <div>内容</div>
</el-tooltip>

四、不同场景的实现方式

场景一:平铺树组件(el-tree)

特点:容器元素与树组件在同一层级,可直接通过 ref 获取

实现步骤

  1. 给树容器添加 ref
vue 复制代码
<div class="tree-container" ref="treeContainerRef">
  <el-tree>...</el-tree>
</div>
  1. 定义容器引用
javascript 复制代码
const treeContainerRef = ref<HTMLElement | null>(null)
  1. 传递给子组件
vue 复制代码
<YlAvatorCard :boundaryEl="treeContainerRef" ... />

场景二:下拉选择组件(el-tree-select)

特点:下拉面板通过 teleport 挂载到 body,容器元素需要动态获取

关键点 :不能使用 el-tree-select 的 ref 作为边界,因为下拉面板是独立挂载的 DOM 元素

实现步骤

  1. 定义边界容器引用
javascript 复制代码
const dialogBoundaryEl = ref<HTMLElement | null>(null)
  1. 在下拉框打开时动态获取
javascript 复制代码
const dialogVisibleChange = (show: boolean) => {
  if (show) {
    nextTick(() => {
      // 获取下拉面板作为边界容器
      dialogBoundaryEl.value = document.querySelector('.area-or-group-tree')
    })
  }
}
  1. 传递给子组件
vue 复制代码
<el-tree-select @visible-change="dialogVisibleChange" ...>
  <template #default="{ data }">
    <YlAvatorCard :boundaryEl="dialogBoundaryEl" ... />
  </template>
</el-tree-select>

五、子组件封装(ylAvatorCard)

Props 定义

typescript 复制代码
defineProps<{
  // ... 其他 props
  boundaryEl?: HTMLElement | null // tooltip 边界容器元素
}>()

Tooltip 配置

vue 复制代码
<el-tooltip
  :disabled="!props.disabled || !props.content"
  :content="props.content"
  placement="top"
  v-model:visible="isTooltipVisible"
  virtual-triggering
  :virtual-ref="cardRef"
  :popper-options="tooltipPopperOptions"
>&nbsp;</el-tooltip>
javascript 复制代码
const cardRef = ref<HTMLElement | null>(null)

const tooltipPopperOptions = computed(() => ({
  modifiers: [
    {
      name: 'preventOverflow',
      options: {
        boundary: props.boundaryEl || cardRef.value || undefined,
      },
    },
    {
      name: 'flip',
      options: {
        boundary: props.boundaryEl || cardRef.value || undefined,
      },
    },
  ],
}))

六、关键技术点总结

技术 作用
virtual-triggering 启用虚拟触发模式,配合 virtual-ref 使用
virtual-ref 指定虚拟触发的 DOM 元素
preventOverflow 防止 tooltip 溢出指定边界
flip 当空间不足时自动翻转位置
nextTick 确保 DOM 渲染完成后再获取元素

七、注意事项

  1. 边界容器选择:必须选择实际包含触发元素的容器,而非组件实例
  2. teleport 组件:下拉面板等 teleport 组件的边界容器需要通过选择器动态获取
  3. 事件清理:动态添加的事件监听需要在组件销毁或下拉框关闭时移除

代码样例(更精细化处理箭头的位置)

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tooltip 位置修复演示</title>
  <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script src="https://unpkg.com/element-plus"></script>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background-color: #f5f5f5;
    }
    .container {
      width: 400px;
      height: 400px;
      border: 1px solid #dcdfe6;
      border-radius: 4px;
      background: #fff;
      display: flex;
      flex-direction: column;
      overflow-x: auto;
      overflow-y: hidden;
    }
    .item {
      padding: 12px 16px;
      border-bottom: 1px solid #ebeef5;
      white-space: nowrap;
      min-width: max-content;
    }
    .item:last-child {
      border-bottom: none;
    }
    .item-text {
      cursor: pointer;
      color: #409eff;
    }
    .tip {
      margin-bottom: 16px;
      padding: 12px;
      background: #fdf6ec;
      border-radius: 4px;
      color: #e6a23c;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <div id="app">
    <h3 style="margin-bottom: 16px;">Tooltip 位置修复演示</h3>
    <div class="tip">
      提示:tooltip 内容和箭头始终保持在容器可视区域内
    </div>
    <div class="container" ref="containerRef">
      <template v-if="isReady">
        <div class="item" v-for="(item, index) in textList" :key="index">
          <el-tooltip 
            :content="item" 
            placement="top"
            effect="dark"
            :popper-options="tooltipPopperOptions"
          >
            <span class="item-text">{{ item }}</span>
          </el-tooltip>
        </div>
      </template>
    </div>
  </div>

  <script>
    const { createApp, ref, computed, onMounted, nextTick } = Vue;
    
    const app = createApp({
      setup() {
        // 容器引用
        const containerRef = ref(null);
        
        // 是否准备好渲染
        const isReady = ref(false);

        // 文本列表
        const textList = ref([
          '这是一段短文本',
          '这是一段稍微长一点的文本内容,用于测试显示效果',
          '这是一段非常长的文本内容,它将会超出容器的宽度范围,从而触发横向滚动条的显示,用户可以通过横向滚动来查看完整的文本内容,鼠标悬浮时会显示完整的提示信息',
          '这是最后一段文本,长度也比较适中,用于演示不同长度文本在容器中的显示效果,同时验证横向滚动功能的正确性'
        ]);

        // 自定义 modifier:限制箭头位置在容器内
        const arrowBoundaryModifier = {
          name: 'arrowBoundary',
          enabled: true,
          phase: 'main',
          fn({ state }) {
            if (!containerRef.value) return;
            
            const containerRect = containerRef.value.getBoundingClientRect();
            const arrowElement = state.elements.arrow;
            
            if (arrowElement && state.modifiersData.arrow) {
              const arrowData = state.modifiersData.arrow;
              const popperRect = state.rects.popper;
              const popperX = state.modifiersData.popperOffsets.x;
              
              // 计算箭头在视口中的实际位置
              const arrowX = popperX + arrowData.x;
              
              // 计算容器边界
              const containerLeft = containerRect.left + 12; // 加 padding
              const containerRight = containerRect.right - 12;
              
              // 限制箭头位置在容器内
              if (arrowX < containerLeft) {
                arrowData.x = containerLeft - popperX;
              } else if (arrowX > containerRight) {
                arrowData.x = containerRight - popperX;
              }
            }
          },
        };

        // Popper 配置:使用容器作为边界约束
        const tooltipPopperOptions = computed(() => {
          if (!containerRef.value) return {};
          return {
            modifiers: [
              {
                name: 'preventOverflow',
                options: {
                  boundary: containerRef.value,
                  padding: 8,
                  tether: false,
                },
              },
              {
                name: 'flip',
                options: {
                  boundary: containerRef.value,
                  padding: 8,
                  fallbackPlacements: ['bottom'],
                },
              },
              {
                name: 'arrow',
                options: {
                  padding: 12,
                },
              },
              arrowBoundaryModifier,
            ],
          };
        });

        // 组件挂载后设置 ready
        onMounted(() => {
          nextTick(() => {
            isReady.value = true;
          });
        });

        return {
          containerRef,
          isReady,
          textList,
          tooltipPopperOptions
        };
      }
    });

    app.use(ElementPlus);
    app.mount('#app');
  </script>
</body>
</html>
相关推荐
JustHappy5 小时前
古法编程秘籍(二):什么是代码模块化?别背概念,把房间收拾明白就够了
前端·后端
小江的记录本6 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
weixin_471383036 小时前
图片预解码缓存
前端·浏览器缓存·图片预解码
岁月宁静7 小时前
驾驭 AI 这匹野马:深入解析智能体 Harness 工程
vue.js·python
郑洁文8 小时前
基于网络爬虫的Web敏感信息泄露自动化检测工具
前端·爬虫·网络安全·自动化
郑洁文8 小时前
可视化Web渗透分析工具的设计与实现
前端
罗超驿8 小时前
18.Web API 实战:元素与表单属性的获取和修改
开发语言·前端·javascript
边界条件╝8 小时前
微前端进阶(四)
前端·状态模式
无风听海8 小时前
JSON Web Token(JWT)完全指南
java·前端·json
IT_陈寒9 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端