Foreword
For front-end developers who frequently build enterprise management backends, they are bound to encounter the requirement of displaying a tooltip when table cell content is truncated. Of course, mature UI frameworks have already addressed this use case. 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 associated code may not be suitable for 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 and are provided solely to illustrate the concepts discussed in the article.
In the above image, you can see a typical table layout commonly found in admin dashboard systems. Since the Ant Design Vue framework is being used, as outlined in the official documentation: 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” ’. Actual code 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’ for fields with potentially long 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 above figure, it can be seen that Element Plus indeed addresses the issue of displaying tooltips for table cells based on whether their content overflows. After the demo verification according to the above configuration, it was also found that it was indeed available. According to the official documents and some source code in the warehouse, found a third-party js library
! Some screenshots of @ popper/core in Element-Plus warehouse
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 to write the Hooks needed in 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 decomposition: width calculation of judgment logic
After reviewing the documentation and source code of ‘Element Plus’, I found that it only provides a configuration for automatic ellipsis in the ‘Table’ component. For other scenarios, we usually use the ‘tooltip’ component, and this method does not consider whether the actual content is exceeded. It is not possible to dynamically decide 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 is a disassembly of some parts of the implementation of this Hooks
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 you need to get the parent element Padding? Here are 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 that includes ‘startContainer’ and ‘endContainer’; here, 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 the range from the document that created it.
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 Decomposition: Create 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 is removed, the popperInstance is destroyed and the mouse is removed from the listening event.
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>
To ensure consistency, we plan to use dynamic tooltips for all rendered content in the management system—excluding tables—and, in conjunction with Hooks, encapsulate this functionality into an ‘EllipsisPopper’ component.
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 ~
because the actual project needs to be compatible with ecological applications (DingTalk, flying books), etc., some fields related to enterprise architecture need to be displayed according to the corresponding development platform. the’ EllipsisPopper’ component shown in the article is only easy to understand, and the content extracted by 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 .e. the specified display area) ‘, and add’ @ mouseenter = “handleCellMouseEnter”’
Finally, post the complete code ~
useEllipsisPopper.js complete code
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,
};
}
Additional styles need to be 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;
}
Reference Links
-Popper.js -Ant Design Vue Table Properties -Element-Plus Table Properties -Element Plus Tooltip Attributes -CSSOM View Module related collation -Scope in DOM