优化tooltip组件提示框的弹出优化显示,解决鼠标从目标元素移动到提示框的过程中提示框消失

This commit is contained in:
2025-06-01 23:53:07 +08:00
parent 1f21c4c2d5
commit 820baa5b22
3 changed files with 162 additions and 48 deletions
@@ -320,6 +320,7 @@ const ReactTableTooltip = ({ content }: { content: string }) => {
trigger="hover" trigger="hover"
showArrow={true} showArrow={true}
className="tooltip-custom-offset" className="tooltip-custom-offset"
// fixedPlacement={true}
> >
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}> <div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
{content} {content}
@@ -619,6 +620,7 @@ export function ReviewPointsList({
trigger="hover" trigger="hover"
showArrow={true} showArrow={true}
className="tooltip-custom-offset tooltip-top" className="tooltip-custom-offset tooltip-top"
fixedPlacement={true}
> >
<span className="status-badge status-success text-xs m-1"> <span className="status-badge status-success text-xs m-1">
<i className="ri-checkbox-circle-line mr-1"></i> <i className="ri-checkbox-circle-line mr-1"></i>
@@ -642,6 +644,7 @@ export function ReviewPointsList({
trigger="hover" trigger="hover"
showArrow={true} showArrow={true}
className="tooltip-custom-offset tooltip-top" className="tooltip-custom-offset tooltip-top"
fixedPlacement={true}
> >
<span className="status-badge status-warning text-xs m-1"> <span className="status-badge status-warning text-xs m-1">
<i className="ri-alert-line mr-1"></i> <i className="ri-alert-line mr-1"></i>
@@ -661,6 +664,7 @@ export function ReviewPointsList({
trigger="hover" trigger="hover"
showArrow={true} showArrow={true}
className="tooltip-custom-offset tooltip-top" className="tooltip-custom-offset tooltip-top"
fixedPlacement={true}
> >
<span className="status-badge status-error text-xs m-1"> <span className="status-badge status-error text-xs m-1">
<i className="ri-close-circle-line mr-1"></i> <i className="ri-close-circle-line mr-1"></i>
+132 -26
View File
@@ -23,6 +23,7 @@ export interface TooltipProps {
maxWidth?: number | string; // 最大宽度 maxWidth?: number | string; // 最大宽度
className?: string; // 自定义类名 className?: string; // 自定义类名
onVisibleChange?: (visible: boolean) => void; // 显示状态变化回调 onVisibleChange?: (visible: boolean) => void; // 显示状态变化回调
fixedPlacement?: boolean; // 是否固定显示位置,不自动切换
} }
/** /**
@@ -43,7 +44,8 @@ export function Tooltip({
showArrow = true, showArrow = true,
maxWidth = 320, maxWidth = 320,
className = '', className = '',
onVisibleChange onVisibleChange,
fixedPlacement = false // 默认不固定位置
}: TooltipProps) { }: TooltipProps) {
// 使用内部状态管理提示框显示状态(非受控模式) // 使用内部状态管理提示框显示状态(非受控模式)
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@@ -58,12 +60,20 @@ export function Tooltip({
const triggerRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null); const tooltipRef = useRef<HTMLDivElement>(null);
// 保存当前实际显示位置
const [actualPlacement, setActualPlacement] = useState<TooltipPlacement>(placement);
// 处理显示状态变化 // 处理显示状态变化
const handleVisibleChange = (newVisible: boolean) => { const handleVisibleChange = (newVisible: boolean) => {
if (!isControlled) { if (!isControlled) {
setVisible(newVisible); setVisible(newVisible);
} }
onVisibleChange?.(newVisible); onVisibleChange?.(newVisible);
// 当显示状态变为false时,重置actualPlacement为初始placement
if (!newVisible) {
setActualPlacement(placement);
}
}; };
// 触发器事件处理 // 触发器事件处理
@@ -95,7 +105,10 @@ export function Tooltip({
const arrowSize = 8; // 箭头大小 const arrowSize = 8; // 箭头大小
const gap = 10; // 提示框与触发元素的间距 const gap = 10; // 提示框与触发元素的间距
switch (placement) { // 使用actualPlacement而不是placement来计算位置
let currentPlacement = actualPlacement;
switch (currentPlacement) {
case 'top': case 'top':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2); left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
top = triggerRect.top - tooltipRect.height - arrowSize; top = triggerRect.top - tooltipRect.height - arrowSize;
@@ -104,7 +117,7 @@ export function Tooltip({
break; break;
case 'bottom': case 'bottom':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2); left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
top = triggerRect.bottom + arrowSize; top = triggerRect.bottom + arrowSize - 8;
arrowLeft = tooltipRect.width / 2 - arrowSize; arrowLeft = tooltipRect.width / 2 - arrowSize;
arrowTop = -arrowSize; arrowTop = -arrowSize;
break; break;
@@ -135,27 +148,105 @@ export function Tooltip({
arrowLeft = Math.min(triggerRect.left + (triggerRect.width / 2) - left - arrowSize, tooltipRect.width - arrowSize * 2); arrowLeft = Math.min(triggerRect.left + (triggerRect.width / 2) - left - arrowSize, tooltipRect.width - arrowSize * 2);
} }
if (top < 0) { // 如果fixedPlacement为true,则强制使用指定的位置,不自动切换
if (placement === 'top') { if (!fixedPlacement) {
// 如果上方放不下,切换到下方 // 自动调整位置逻辑
top = triggerRect.bottom + arrowSize; if (top < 0) {
arrowTop = -arrowSize; if (currentPlacement === 'top') {
tooltipRef.current.classList.remove('tooltip-top'); // 如果上方放不下,切换到下方
tooltipRef.current.classList.add('tooltip-bottom'); top = triggerRect.bottom + arrowSize;
} else { arrowTop = -arrowSize;
top = gap;
arrowTop = Math.max(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, arrowSize * 2); // 更新实际位置为bottom
setActualPlacement('bottom');
currentPlacement = 'bottom';
// 更新CSS类
tooltipRef.current.classList.remove('tooltip-top');
tooltipRef.current.classList.add('tooltip-bottom');
} else {
top = gap;
arrowTop = Math.max(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, arrowSize * 2);
}
} else if (top + tooltipRect.height > viewportHeight) {
if (currentPlacement === 'bottom') {
// 如果下方放不下,切换到上方
top = triggerRect.top - tooltipRect.height - arrowSize;
arrowTop = tooltipRect.height;
// 更新实际位置为top
setActualPlacement('top');
currentPlacement = 'top';
// 更新CSS类
tooltipRef.current.classList.remove('tooltip-bottom');
tooltipRef.current.classList.add('tooltip-top');
} else {
top = viewportHeight - tooltipRect.height - gap;
arrowTop = Math.min(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, tooltipRect.height - arrowSize * 2);
}
} }
} else if (top + tooltipRect.height > viewportHeight) {
if (placement === 'bottom') { // 处理左右方向的切换
// 如果下方放不下,切换到上方 if (left < 0 && currentPlacement === 'left') {
top = triggerRect.top - tooltipRect.height - arrowSize; // 如果左边放不下,切换到右边
arrowTop = tooltipRect.height; left = triggerRect.right + arrowSize;
tooltipRef.current.classList.remove('tooltip-bottom'); arrowLeft = -arrowSize;
tooltipRef.current.classList.add('tooltip-top');
} else { // 更新实际位置为right
top = viewportHeight - tooltipRect.height - gap; setActualPlacement('right');
arrowTop = Math.min(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, tooltipRect.height - arrowSize * 2); currentPlacement = 'right';
// 更新CSS类
tooltipRef.current.classList.remove('tooltip-left');
tooltipRef.current.classList.add('tooltip-right');
} else if (left + tooltipRect.width > viewportWidth && currentPlacement === 'right') {
// 如果右边放不下,切换到左边
left = triggerRect.left - tooltipRect.width - arrowSize;
arrowLeft = tooltipRect.width;
// 更新实际位置为left
setActualPlacement('left');
currentPlacement = 'left';
// 更新CSS类
tooltipRef.current.classList.remove('tooltip-right');
tooltipRef.current.classList.add('tooltip-left');
}
} else {
// 固定位置,但仍然需要确保在视口内可见
if (currentPlacement === 'top') {
// 如果提示框超出了顶部,增加偏移
if (top < 0) {
const originalTop = top;
top = gap;
// 调整箭头位置,使其指向触发元素
arrowTop = arrowTop + (originalTop - top);
}
} else if (currentPlacement === 'bottom') {
// 如果提示框超出了底部,减少偏移
if (top + tooltipRect.height > viewportHeight) {
const originalTop = top;
top = viewportHeight - tooltipRect.height - gap;
// 调整箭头位置,使其指向触发元素
arrowTop = arrowTop - (originalTop - top);
}
} else if (currentPlacement === 'left') {
// 如果提示框超出了左侧,增加偏移
if (left < 0) {
const originalLeft = left;
left = gap;
// 调整箭头位置,使其指向触发元素
arrowLeft = arrowLeft + (originalLeft - left);
}
} else if (currentPlacement === 'right') {
// 如果提示框超出了右侧,减少偏移
if (left + tooltipRect.width > viewportWidth) {
const originalLeft = left;
left = viewportWidth - tooltipRect.width - gap;
// 调整箭头位置,使其指向触发元素
arrowLeft = arrowLeft - (originalLeft - left);
}
} }
} }
@@ -168,35 +259,49 @@ export function Tooltip({
// 定位箭头 // 定位箭头
const arrow = tooltipRef.current.querySelector('.tooltip-arrow') as HTMLElement; const arrow = tooltipRef.current.querySelector('.tooltip-arrow') as HTMLElement;
if (arrow && showArrow) { if (arrow && showArrow) {
switch (placement) { // 使用当前实际的位置来定位箭头
switch (currentPlacement) {
case 'top': case 'top':
arrow.style.bottom = `-${arrowSize}px`; arrow.style.bottom = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`; arrow.style.left = `${arrowLeft}px`;
arrow.style.top = 'auto'; arrow.style.top = 'auto';
arrow.style.right = 'auto'; arrow.style.right = 'auto';
// 设置箭头指向下方的样式
arrow.className = 'tooltip-arrow tooltip-arrow-bottom';
break; break;
case 'bottom': case 'bottom':
arrow.style.top = `-${arrowSize}px`; arrow.style.top = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`; arrow.style.left = `${arrowLeft}px`;
arrow.style.bottom = 'auto'; arrow.style.bottom = 'auto';
arrow.style.right = 'auto'; arrow.style.right = 'auto';
// 设置箭头指向上方的样式
arrow.className = 'tooltip-arrow tooltip-arrow-top';
break; break;
case 'left': case 'left':
arrow.style.right = `-${arrowSize}px`; arrow.style.right = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`; arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto'; arrow.style.bottom = 'auto';
arrow.style.left = 'auto'; arrow.style.left = 'auto';
// 设置箭头指向右方的样式
arrow.className = 'tooltip-arrow tooltip-arrow-right';
break; break;
case 'right': case 'right':
arrow.style.left = `-${arrowSize}px`; arrow.style.left = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`; arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto'; arrow.style.bottom = 'auto';
arrow.style.right = 'auto'; arrow.style.right = 'auto';
// 设置箭头指向左方的样式
arrow.className = 'tooltip-arrow tooltip-arrow-left';
break; break;
} }
} }
}; };
// 当组件首次挂载或placement改变时,重置actualPlacement
useEffect(() => {
setActualPlacement(placement);
}, [placement]);
// 计算提示框位置 // 计算提示框位置
useEffect(() => { useEffect(() => {
if (!isVisible) return; if (!isVisible) return;
@@ -217,7 +322,7 @@ export function Tooltip({
window.removeEventListener('resize', updateTooltipPosition); window.removeEventListener('resize', updateTooltipPosition);
clearInterval(intervalId); clearInterval(intervalId);
}; };
}, [isVisible, placement, maxWidth, showArrow]); }, [isVisible, placement, maxWidth, showArrow, fixedPlacement, actualPlacement]);
// 处理点击外部关闭 // 处理点击外部关闭
useEffect(() => { useEffect(() => {
@@ -262,10 +367,11 @@ export function Tooltip({
// 生成提示框类名 // 生成提示框类名
const tooltipClassNames = [ const tooltipClassNames = [
'tooltip-container', 'tooltip-container',
`tooltip-${placement}`, `tooltip-${actualPlacement}`, // 使用actualPlacement而不是placement
`tooltip-${theme}`, `tooltip-${theme}`,
rich ? 'tooltip-rich' : '', rich ? 'tooltip-rich' : '',
isVisible ? 'tooltip-visible' : '', isVisible ? 'tooltip-visible' : '',
fixedPlacement ? 'tooltip-fixed-placement' : '',
className className
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
+26 -22
View File
@@ -103,27 +103,48 @@
border-color: #3b82f6; border-color: #3b82f6;
} }
/* Tooltip 位置基本样式 */ /* Tooltip 位置基本样式 - 根据箭头方向而不是容器位置 */
.tooltip-top .tooltip-arrow { /* 箭头朝下 - 用于顶部提示框 */
.tooltip-arrow-bottom {
border-width: 8px 8px 0 8px; border-width: 8px 8px 0 8px;
border-color: inherit transparent transparent transparent; border-color: inherit transparent transparent transparent;
} }
.tooltip-bottom .tooltip-arrow { /* 箭头朝上 - 用于底部提示框 */
.tooltip-arrow-top {
border-width: 0 8px 8px 8px; border-width: 0 8px 8px 8px;
border-color: transparent transparent inherit transparent; border-color: transparent transparent inherit transparent;
} }
.tooltip-left .tooltip-arrow { /* 箭头朝右 - 用于左侧提示框 */
.tooltip-arrow-right {
border-width: 8px 0 8px 8px; border-width: 8px 0 8px 8px;
border-color: transparent transparent transparent inherit; border-color: transparent transparent transparent inherit;
} }
.tooltip-right .tooltip-arrow { /* 箭头朝左 - 用于右侧提示框 */
.tooltip-arrow-left {
border-width: 8px 8px 8px 0; border-width: 8px 8px 8px 0;
border-color: transparent inherit transparent transparent; border-color: transparent inherit transparent transparent;
} }
/* 浅色主题下的箭头样式修正 */
.tooltip-light .tooltip-arrow-bottom {
border-color: #ffffff transparent transparent transparent;
}
.tooltip-light .tooltip-arrow-top {
border-color: transparent transparent #ffffff transparent;
}
.tooltip-light .tooltip-arrow-right {
border-color: transparent transparent transparent #ffffff;
}
.tooltip-light .tooltip-arrow-left {
border-color: transparent #ffffff transparent transparent;
}
/* 富文本提示框样式 */ /* 富文本提示框样式 */
.tooltip-rich .tooltip-content { .tooltip-rich .tooltip-content {
padding: 0; padding: 0;
@@ -168,23 +189,6 @@
border-top-color: #e2e8f0; border-top-color: #e2e8f0;
} }
/* 浅色主题下的箭头样式修正 */
.tooltip-light.tooltip-top .tooltip-arrow {
border-color: #ffffff transparent transparent transparent;
}
.tooltip-light.tooltip-bottom .tooltip-arrow {
border-color: transparent transparent #ffffff transparent;
}
.tooltip-light.tooltip-left .tooltip-arrow {
border-color: transparent transparent transparent #ffffff;
}
.tooltip-light.tooltip-right .tooltip-arrow {
border-color: transparent #ffffff transparent transparent;
}
/* 表格样式 */ /* 表格样式 */
.tooltip-content table { .tooltip-content table {
border-collapse: collapse; border-collapse: collapse;