import React, { useRef, useEffect, HTMLProps } from "react";
import { ReferenceObject } from "popper.js";
import { Bubble, BubbleContentProps, BubbleProps } from "../bubble";
import { TriggerProps } from "../popover";
import { isMobile } from "../_util/is-mobile";
/**
* 使用 Tooltip 的内容需要使用容器包裹。
*
* 默认会使用 span 包裹内容,如有需要,可以改为使用 div 包裹。
*/
export interface TooltipProps {
/**
* Tooltip 放置位置
* @default "bottom-start"
*/
placement?: BubbleContentProps["placement"];
/**
* 文本提示内容
*/
title?: React.ReactNode;
/**
* 要提示的对象
*/
children?: React.ReactNode;
/**
* 出现延迟时间(ms)
* @default 600
*/
openDelay?: number;
/**
* 距上次 Tooltip 出现 1s 内再次出现延迟时间(ms)
* @default 300
*/
openDelayWhenHasInstance?: number;
/**
* 挂载浮层组件的节点
* @default document.body
* @since 2.5.4
*/
popupContainer?: BubbleProps["popupContainer"];
}
let visibleTooltipCount = 0;
/**
* 控制 Tooltip 的触发机制
*
* Tooltip 跟普通的 Bubble 不一样:
*
* 1. 出现位置不参考元素,参考鼠标
* 2. 如果场上无 Tooltip 实例,则激活后 0.6s 后弹出
* 3. 如果场上有 Tooltip 实例,则激活后 0.3s 后弹出
*
* 设计上面的规则,是为了对齐系统 Tooltip 的交互
*/
const TooltipTrigger = ({
delay,
openDelay = delay,
closeDelay = 0,
setVisible,
render,
openDelayWhenHasInstance,
}: TriggerProps & { delay: number; openDelayWhenHasInstance: number }) => {
// 当前鼠标的位置,用作 Tooltip 弹出位置的参考
const mouseElement = useRef<
ReferenceObject & { scrollTop: number; scrollLeft: number }
>(null);
// Tooltip 确定要激活,在延时出来期间,需要监听鼠标新位置,这个引用保存监听的取消方法
const mouseWatching = useRef<() => void>(null);
// 从鼠标事件获得的坐标信息,构造鼠标参考位置
const mark = useRef((evt: MouseEvent) => {
const width = 0;
const height = 0;
const clientX = Math.round(evt.clientX);
const clientY = Math.round(evt.clientY);
mouseElement.current = {
clientWidth: width,
clientHeight: height,
getBoundingClientRect: () => ({
left: clientX,
top: clientY,
right: clientX + width,
bottom: clientY + height,
width,
height,
}),
// IE10 下 Popper 读取这两个值计算
scrollTop: 0,
scrollLeft: 0,
};
});
// 监听鼠标位置变化,更新位置
const watch = () => {
document.addEventListener("mousemove", mark.current);
mouseWatching.current = () =>
document.removeEventListener("mousemove", mark.current);
return mouseWatching.current;
};
// 取消当前的位置变化监听
const unwatch = () => {
if (mouseWatching.current) {
mouseWatching.current();
mouseWatching.current = null;
}
};
// 用户鼠标进入目标元素
const enter = async (evt: React.MouseEvent) => {
// 记录鼠标位置并跟踪
mark.current(evt.nativeEvent);
watch();
// Tooltip 出来后,更新实例数量,并且可以取消跟踪了(Tooltip 出来的时候,鼠标在哪儿,就在哪儿,无需跟随)
await setVisible(
true,
visibleTooltipCount > 0 ? openDelayWhenHasInstance : openDelay
);
visibleTooltipCount += 1;
unwatch();
};
// 用户鼠标离开目标元素
const leave = async () => {
// 取消原有的跟踪
unwatch();
// 完全隐藏后,延时 1 秒更新实例数量
// 这里延时 1 秒是给用户留足够的空间,不至于由于鼠标走的慢导致下次 Tooltip 又要等很久
await setVisible(false, closeDelay);
// CD 1 秒
await new Promise(resolve => setTimeout(resolve, 1000));
visibleTooltipCount -= 1;
};
// 组件 unmount 的时候,清理一切
useEffect(
() => () => {
mouseElement.current = null;
// React 18 会触发卸载🤔,先取消这里
// mark.current = null;
unwatch();
},
[]
);
return render({
childrenProps: {
onMouseEnter: enter,
onMouseLeave: leave,
},
overlayProps: {},
referenceElement: mouseElement.current,
});
};
TooltipTrigger.displayName = "TooltipTrigger";
function TooltipMobileTrigger(props: TriggerProps) {
const { setVisible, openDelay = 1000, closeDelay = 100, render } = props;
const commonProps: HTMLProps = {
onTouchStart: () => setVisible(true, openDelay),
onTouchEnd: () => setVisible(false, closeDelay),
};
return render({
overlayProps: commonProps,
childrenProps: commonProps,
});
}
TooltipMobileTrigger.displayName = "TooltipTrigger";
export function Tooltip({
title,
placement = "bottom-start",
children,
openDelay = 600,
openDelayWhenHasInstance = 300,
popupContainer,
}: TooltipProps) {
return (
{children}
);
}
Tooltip.displayName = "Tooltip";