HOOKS

Text overflow prompt useEllipsisPopper for public Hooks encapsulation

Public Hook Encapsulation: Text Overflow Tooltip – useEllipsisPopper

Foreword

For front-end development that often needs to develop enterprise management background, it is bound to encounter the requirement scenario of prompting floating windows after the display fields in the form are omitted. Of course, the mature UI framework has also solved this requirement scenario. However, for the entire project or system, how similar scenarios can be better reused, and the UI framework currently in use may not meet the requirements, so this Hooks and the corresponding extended business components are encapsulated in combination with the actual business.

Hooks encapsulated based on a personal project environment; this article only shares insights and best practices for hook encapsulation, so the accompanying code may not be directly applicable to others.

Project Environment

Vue 3.x + Ant Design Vue 3.x + Vite 4.x

Business Scenario Analysis

The images and text are for reference only, providing only the illustrations needed to support the ideas presented in the article.

! Existing tooltip scenario

In the above picture, it is a common form content in the management background system, because the Ant Design Vue framework is used, according to the official document: The API ‘ellipsis’ of ‘Column’ is automatically omitted beyond the width, and it is not supported to be used with sorting and filtering, and the table layout will become ’tableLayout = “fixed” ’. Code actually used:

[
  {
      title: '所属角色',
      key: 'role',
      width: 100,
    },
    {
      title: '所在部门',
      key: 'department',
      width: 160,
      ellipsis: true
    }
]

From the above figure, a problem is exposed, that is, because the’ column’ is passed into the table component as a “configuration item”, after configuration’ ellipsis: true’ is configured for fields with a long number of words, the’ tooltip’ will be rendered regardless of whether the text content exceeds the width of the table column. in terms of experience and performance, it is not necessarily good, rendering some “meaningless” DOM.

Similarly, in the middle and background management system, due to business considerations or UI interface design and other reasons, some display areas need to display field contents that may be too long, while Ant Design Vue, which is matched according to technical selection, provides the’ tooltip’ component and still has the above problems.

How Does Element Plus Do It?

As one of the popular UI frameworks at the front end, ElementUI the table content of Plus, we can find the corresponding content from its documents for how to do the above scenes ~

! Element Plus Table Properties

In the figure above, we can see that Element Plus has indeed addressed the issue of displaying tooltips for table cells based on whether their content overflows. Subsequently, demo verification based on the above configuration also confirmed that it is indeed functional. Based on the official documentation and some of the source code in the repository, I discovered a third-party JavaScript library.

! Partial screenshot from the Element Plus repository showing @popper/core

Popper.js

TOOLTIP & POPOVER POSITIONING ENGINE

From the official documents and various tutorials searched out, it is not difficult to understand that this is a tooltips prompt JS plug-in with good expansibility. its size is only about 3.5KB, and its use and configuration are quite simple. there are also many component libraries based on’ popper.js’ package. this part of the content is not the focus of the article, and there have been many introductions to its principle and other related excellent contents. here, I will not repeat ~

After understanding what this is for, I began implementing the custom hooks needed for the project. Using ‘popper.js’ mainly uses the ’createPopper()’method, which accepts 3 parameters: ‘reference’ (the button Element that needs the pop-up box), ‘popper’ (the tooltip content HTMLElement), and ‘options’

The’ placement’ (direction) and’ modifiers’ are mainly used in the’ options’, and the’ name’ and’ offset’ are used in Hooks. Other configuration parameters are not considered. For more complete and complex packages, you can view excellent component (method) libraries such as’ Element Plus’ or’ Tippy.js.

 const popperInstance = createPopper(parent, tooltipContent, {
    placement: options.placement ?? 'top',
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: [0, 8],
        },
      },
    ],
  });

Encapsulation and Decomposition: Width Calculation in Decision Logic

After reviewing the documentation and source code of Element Plus, I found that it only provides an auto-ellipsis configuration for the Table component. As for other scenarios, we typically use the ‘tooltip’ component, but this approach does not account for whether the actual content overflows. It is not possible to dynamically determine whether to display the tooltip.

The practice in Hooks is based on [width of child element + width of parent element> padding of parent element? Show tooltip: no show]]

The following content provides a breakdown of some of the implementation details for this Hook.

const getPadding = el => {
  const style = window.getComputedStyle(el, null);
  const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
  const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
  const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
  const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
  return {
    left: paddingLeft,
    right: paddingRight,
    top: paddingTop,
    bottom: paddingBottom,
  };
};

Why do we need to retrieve the parent element’s padding? This brings us to some questions about BFC.

! BFC

Determine when the child element needs to be hidden and displayed. According to the above figure, when the width of ‘Child container’ + the padding of the yellow area> the width of ‘Parent container’, the tooltip is generated.

let range = document.createRange();
range.setStart(target, 0);
range.setEnd(target, target.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
range.detach();
const { left, right } = getPadding(target);
const horizontalPadding = left + right;

’document.createRange()’is used to create a ‘Range’ object, containing ‘startContainer’ and ‘endContainer’, where we use ‘setStart’ and ‘setEnd’ Create the selected DOM range to obtain the ‘rangeWidth’ for subsequent comparison and calculation. After using the range, call the detach() method to detach it from the document that created the range.

For this part of the content and the specific knowledge related to CSSOM view, you can view the article by Zhang Xinxu, whose address is here:CSSOM View Module (CSSOM View Module) Related Arrangement

Encapsulation and Decomposition: Creating tooltipContent

The preconditions for generating the tooltip are determined. What is to be displayed in the tooltip content? This Hooks uses the defined attribute ‘data-title’ obtained when the mouse is moved in and assigns it’ innerText’ to create’ tooltipContent’ and’ arrowContent’ according to the document of popper.js’.

const renderContent = (target, parent) => {
  const tooltipContent = document.createElement('div');
  const arrowContent = document.createElement('div');
  arrowContent.className = ['ellipsis-tooltip-arrow'].join(' ');
  arrowContent.setAttribute('data-popper-arrow', 'true');
  tooltipContent.innerText = target.dataset.title;
  tooltipContent.setAttribute('role', 'tooltip');
  tooltipContent.appendChild(arrowContent);
  tooltipContent.className = ['ellipsis-tooltip'].join(' ');
  parent.setAttribute('aria-describedby', 'tooltip');
  parent.appendChild(tooltipContent);
  return {
    tooltipContent,
  };
};

Similarly, when the mouse leaves, destroy the popperInstance and remove the mouse‑out event listener.

  popperInstance.destroy();
  parent.removeChild(tooltipContent);
  parent.removeAttribute('aria-describedby');
  target.removeListener('mouseleave', removePopper);

Component Decomposition: EllipsisPopper.vue Component

<template>
  <div class="ellipsis" :data-title="text" @mouseenter="handleCellMouseEnter">
    <span>{{ text }}</span>
  </div>
</template>

<script setup>
  import { useEllipsisPopper } from '@/hooks';

  defineProps({
    text: {
      type: String,
      required: true,
    },
  });

  const { handleCellMouseEnter } = useEllipsisPopper({ placement: 'auto' });
</script>

Because it is considered that the dynamic display tooltip will be used in the management system for other rendering contents other than tables, which will be used together with Hooks to encapsulate’ EllipsisPopper’ components.

At this point, the contents needed in Hooks have been clarified. In addition, only a single line of text overflow hidden display tooltip has been considered in Hooks. The demand for multi-line text overflow hidden display tooltip has not been considered. Correspondingly, more complicated configuration such as’ Element Plus’ has not been realized. Hooks itself has not made more complicated expansion in combination with the actual needs of the project.

Finally, post the initial table changes of the article after using the’ EllipsisPopper’ component and’ useEllipsisPopper.js ~

! popper-05

because the actual project needs to be compatible with ecological applications (DingTalk, flying books), etc., it is necessary to display some fields related to enterprise architecture according to the corresponding development platform. the’ EllipsisPopper’ component shown in the article is only easy to understand, and the content extracted from desensitization processing of actual business components. if compatibility with ecological applications is not required, the custom attribute’ data-title’ can be directly given to the parent element (I ‘, and add’ @ mouseenter = “handleCellMouseEnter”’

Finally, here’s the complete code~

Complete code for useEllipsisPopper.js


const getPadding = el => {
  const style = window.getComputedStyle(el, null);
  const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
  const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
  const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
  const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
  return {
    left: paddingLeft,
    right: paddingRight,
    top: paddingTop,
    bottom: paddingBottom,
  };
};

const renderContent = (target, parent) => {
  const tooltipContent = document.createElement('div');
  const arrowContent = document.createElement('div');
  arrowContent.className = ['ellipsis-tooltip-arrow'].join(' ');
  arrowContent.setAttribute('data-popper-arrow', 'true');
  tooltipContent.innerText = target.dataset.title;
  tooltipContent.setAttribute('role', 'tooltip');
  tooltipContent.appendChild(arrowContent);
  tooltipContent.className = ['ellipsis-tooltip'].join(' ');
  parent.setAttribute('aria-describedby', 'tooltip');
  parent.appendChild(tooltipContent);
  return {
    tooltipContent,
  };
};

export function useEllipsisPopper(options = {}) {
  const handleCellMouseEnter = event => {
    const target = event.target;
    const parent = target.parentNode;
    let range = document.createRange();
    range.setStart(target, 0);
    range.setEnd(target, target.childNodes.length);
    const rangeWidth = range.getBoundingClientRect().width;
    range.detach();
    const { left, right } = getPadding(target);
    const horizontalPadding = left + right;
    if (Math.floor(rangeWidth + horizontalPadding) > target.clientWidth) {
      const { tooltipContent } = renderContent(target, parent);
      const popperInstance = createPopper(parent, tooltipContent, {
        placement: options.placement ?? 'top',
        modifiers: [
          {
            name: 'offset',
            options: {
              offset: [0, 8],
            },
          },
        ],
      });

      const removePopper = () => {
        popperInstance.destroy();
        parent.removeChild(tooltipContent);
        parent.removeAttribute('aria-describedby');
        target.removeListener('mouseleave', removePopper);
      };

      target.addEventListener('mouseleave', removePopper);
    }
  };

  return {
    handleCellMouseEnter,
  };
}

Styles that need to be additionally added to the project

.ellipsis-tooltip {
  z-index: 10;
  display: inline-block;
  background: #333333;
  color: #ffffff;
  padding: 5px 10px;
  font-size: 13px;
  border-radius: 4px;
}

.ellipsis-tooltip-arrow,
.ellipsis-tooltip-arrow::before {
  position: absolute;
  width: 6px;
  height: 6px;
  background: inherit;
}

.ellipsis-tooltip-arrow {
  visibility: hidden;
}

.ellipsis-tooltip-arrow::before {
  visibility: visible;
  content: '';
  transform: rotate(45deg);
}

.ellipsis-tooltip[data-popper-placement^='top'] > .ellipsis-tooltip-arrow {
  bottom: -3px;
}

.ellipsis-tooltip[data-popper-placement^='bottom'] > .ellipsis-tooltip-arrow {
  top: -3px;
}

.ellipsis-tooltip[data-popper-placement^='left'] > .ellipsis-tooltip-arrow {
  right: -3px;
}

.ellipsis-tooltip[data-popper-placement^='right'] > .ellipsis-tooltip-arrow {
  left: -3px;
}

Related posts

View all →