-
Notifications
You must be signed in to change notification settings - Fork 3
/
TruncatedTooltip.tsx
132 lines (124 loc) · 3.36 KB
/
TruncatedTooltip.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { Box, Tooltip } from "@mui/material";
import React, {
Children,
ReactElement,
isValidElement,
useRef,
useState
} from "react";
import { TruncatedTooltipProps } from "./TruncatedTooltip.types";
/**
* Truncates text and shows a tooltip if the text overflows.
* Automatically grabs the tooltip from child.
* Renders a specified MUI component or element otherwise wraps string in a span.
*/
const TruncatedTooltip = <T extends React.ElementType = "span">({
children,
component,
multiline,
sx,
tooltip,
alwaysShowTooltip = false,
TooltipProps = undefined,
...rest
}: TruncatedTooltipProps<T>) => {
// Ref to the text element.
const textElementRef = useRef<HTMLInputElement | null>(null);
// State to determine if the tooltip should show.
const [open, setOpen] = useState(false);
/**
* If the text overflows, show the tooltip.
*/
const handleShouldShow = ({
currentTarget
}: React.MouseEvent<HTMLDivElement | null>) => {
setOpen(
currentTarget.scrollWidth > currentTarget.clientWidth ||
currentTarget.scrollHeight > currentTarget.clientHeight ||
alwaysShowTooltip
);
};
/**
* Hide the tooltip.
*/
const hideTooltip = () => setOpen(false);
/**
* Returns the text from a child element.
* @param child Valid React element.
* @returns string or null.
*/
const getText = (child: ReactElement): string | null => {
if (isValidElement(child)) {
const props = child.props as { children?: React.ReactNode };
if (props !== null && typeof props?.children === "string") {
return props.children;
} else {
return null;
}
} else {
return null;
}
};
/**
* Extracted text from children.
*/
const text = children
? Children.map(children, child => {
/**
* If the child is a string, return the string.
* If the child is a valid React element, return the text from the element.
* Otherwise, return null.
*/
if (typeof child === "string") {
return child;
} else if (isValidElement(child)) {
return getText(child) || "";
} else {
return null;
}
})
: "";
return (
<Tooltip
{...TooltipProps}
open={open}
title={tooltip || text}
disableHoverListener={!open}
onMouseEnter={handleShouldShow}
onMouseLeave={hideTooltip}
>
<Box
component={component || "span"}
ref={textElementRef}
sx={[
{
"& > *": {
display: "inline"
},
display: "block",
overflow: "hidden",
textDecoration: "none",
textOverflow: "ellipsis",
// Allow wrapping for multiline text
whiteSpace: multiline ? "normal" : "nowrap",
wordBreak: "break-all"
},
// Ensure multiline clamps to specified number of lines
multiline
? {
WebkitBoxOrient: "vertical",
WebkitLineClamp: multiline,
display: "-webkit-box"
}
: {},
// You cannot spread `sx` directly because `SxProps` (typeof sx) can be an array.
...(Array.isArray(sx) ? sx : [sx])
]}
{...rest}
>
{children}
</Box>
</Tooltip>
);
};
export default TruncatedTooltip;