Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 99ac1d1

Browse files
committed
Implemented Tooltip component and added tooltips for user handle and team name.
1 parent 9b10ac2 commit 99ac1d1

File tree

5 files changed

+205
-13
lines changed

5 files changed

+205
-13
lines changed

src/components/ProjectName/styles.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@import "styles/mixins";
22

33
.container {
4-
display: block;
4+
display: inline-block;
55
max-width: 20em;
66
overflow: hidden;
77
text-overflow: ellipsis;

src/components/Tooltip/index.jsx

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { useCallback, useEffect, useRef, useState } from "react";
2+
import { usePopper } from "react-popper";
3+
import PT from "prop-types";
4+
import cn from "classnames";
5+
import compStyles from "./styles.module.scss";
6+
7+
/**
8+
* Displays a tooltip
9+
*
10+
* @param {Object} props component properties
11+
* @param {any} props.children tooltip target
12+
* @param {string} [props.className] class name to be added to root element
13+
* @param {any} props.content tooltip content
14+
* @param {number} [props.delay] postpone showing the tooltip after this delay
15+
* @param {import('@popperjs/core').Placement} [props.placement] tooltip's
16+
* preferred placement as defined in PopperJS documentation
17+
* @param {'absolute'|'fixed'} [props.strategy] tooltip positioning strategy
18+
* as defined in PopperJS documentation
19+
* @param {string} [props.targetClassName] class name to be added to element
20+
* wrapping around component's children
21+
* @param {string} [props.tooltipClassName] class name to be added to tooltip
22+
* element itself
23+
* @returns {JSX.Element}
24+
*/
25+
const Tooltip = ({
26+
children,
27+
className,
28+
content,
29+
delay = 150,
30+
placement = "top",
31+
strategy = "absolute",
32+
targetClassName,
33+
tooltipClassName,
34+
}) => {
35+
const containerRef = useRef(null);
36+
const timeoutIdRef = useRef(0);
37+
const [isTooltipShown, setIsTooltipShown] = useState(false);
38+
const [referenceElement, setReferenceElement] = useState(null);
39+
const [popperElement, setPopperElement] = useState(null);
40+
const [arrowElement, setArrowElement] = useState(null);
41+
const { styles, attributes, update } = usePopper(
42+
referenceElement,
43+
popperElement,
44+
{
45+
placement,
46+
strategy,
47+
modifiers: [
48+
{ name: "arrow", options: { element: arrowElement, padding: 10 } },
49+
{ name: "offset", options: { offset: [0, 10] } },
50+
{ name: "preventOverflow", options: { padding: 15 } },
51+
],
52+
}
53+
);
54+
55+
const onMouseEnter = useCallback(() => {
56+
timeoutIdRef.current = window.setTimeout(() => {
57+
timeoutIdRef.current = 0;
58+
setIsTooltipShown(true);
59+
}, delay);
60+
}, [delay]);
61+
62+
const onMouseLeave = useCallback(() => {
63+
if (timeoutIdRef.current) {
64+
clearTimeout(timeoutIdRef.current);
65+
}
66+
setIsTooltipShown(false);
67+
}, []);
68+
69+
useEffect(() => {
70+
let observer = null;
71+
if (isTooltipShown && popperElement && update) {
72+
observer = new ResizeObserver(update);
73+
observer.observe(popperElement);
74+
}
75+
return () => {
76+
if (observer) {
77+
observer.unobserve(popperElement);
78+
}
79+
};
80+
}, [isTooltipShown, popperElement, update]);
81+
82+
return (
83+
<div
84+
className={cn(compStyles.container, className)}
85+
ref={containerRef}
86+
onMouseEnter={onMouseEnter}
87+
onMouseLeave={onMouseLeave}
88+
>
89+
<span
90+
className={cn(compStyles.target, targetClassName)}
91+
ref={setReferenceElement}
92+
>
93+
{children}
94+
</span>
95+
{isTooltipShown && (
96+
<div
97+
ref={setPopperElement}
98+
className={cn(compStyles.tooltip, tooltipClassName)}
99+
style={styles.popper}
100+
{...attributes.popper}
101+
>
102+
{content}
103+
<div
104+
className={compStyles.tooltipArrow}
105+
ref={setArrowElement}
106+
style={styles.arrow}
107+
/>
108+
</div>
109+
)}
110+
</div>
111+
);
112+
};
113+
114+
Tooltip.propTypes = {
115+
children: PT.node,
116+
className: PT.string,
117+
content: PT.node,
118+
delay: PT.number,
119+
placement: PT.string,
120+
strategy: PT.oneOf(["absolute", "fixed"]),
121+
targetClassName: PT.string,
122+
tooltipClassName: PT.string,
123+
};
124+
125+
export default Tooltip;
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.container {
2+
position: relative;
3+
display: inline-flex;
4+
align-items: baseline;
5+
}
6+
7+
.target {
8+
display: inline-flex;
9+
align-items: baseline;
10+
}
11+
12+
.tooltip {
13+
z-index: 8;
14+
border-radius: 8px;
15+
padding: 10px 15px;
16+
box-shadow: 0px 5px 25px #c6c6c6;
17+
background: #fff;
18+
19+
.tooltipArrow {
20+
display: block;
21+
top: 100%;
22+
border: 10px solid transparent;
23+
border-bottom: none;
24+
border-top-color: #fff;
25+
width: 0;
26+
height: 0;
27+
}
28+
}

src/routes/WorkPeriods/components/PeriodItem/index.jsx

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import React, { memo, useCallback } from "react";
1+
import React, { memo, useCallback, useMemo } from "react";
22
import { useDispatch } from "react-redux";
33
import PT from "prop-types";
44
import cn from "classnames";
55
import debounce from "lodash/debounce";
66
import Checkbox from "components/Checkbox";
7+
import JobName from "components/JobName";
78
import ProjectName from "components/ProjectName";
9+
import Tooltip from "components/Tooltip";
810
import PaymentError from "../PaymentError";
911
import PaymentStatus from "../PaymentStatus";
1012
import PaymentTotal from "../PaymentTotal";
13+
import PeriodWorkingDays from "../PeriodWorkingDays";
1114
import PeriodDetails from "../PeriodDetails";
1215
import { PAYMENT_STATUS } from "constants/workPeriods";
1316
import {
@@ -23,7 +26,6 @@ import { useUpdateEffect } from "utils/hooks";
2326
import { formatUserHandleLink, formatWeeklyRate } from "utils/formatters";
2427
import { stopPropagation } from "utils/misc";
2528
import styles from "./styles.module.scss";
26-
import PeriodWorkingDays from "../PeriodWorkingDays";
2729

2830
/**
2931
* Displays the working period data row to be used in PeriodList component.
@@ -85,6 +87,26 @@ const PeriodItem = ({
8587
updateWorkingDays(data.daysWorked);
8688
}, [data.daysWorked]);
8789

90+
const jobName = useMemo(
91+
() => (
92+
<span className={styles.tooltipContent}>
93+
<span className={styles.tooltipLabel}>Job Title:</span>&nbsp;
94+
<JobName jobId={item.jobId} />
95+
</span>
96+
),
97+
[item.jobId]
98+
);
99+
100+
const projectId = useMemo(
101+
() => (
102+
<span className={styles.tooltipContent}>
103+
<span className={styles.tooltipLabel}>Project ID:</span>&nbsp;
104+
{item.projectId}
105+
</span>
106+
),
107+
[item.projectId]
108+
);
109+
88110
return (
89111
<>
90112
<tr
@@ -106,7 +128,10 @@ const PeriodItem = ({
106128
/>
107129
</td>
108130
<td className={styles.userHandle}>
109-
<span>
131+
<Tooltip
132+
content={jobName}
133+
targetClassName={styles.userHandleContainer}
134+
>
110135
<a
111136
href={formatUserHandleLink(item.projectId, item.rbId)}
112137
onClick={stopPropagation}
@@ -115,10 +140,12 @@ const PeriodItem = ({
115140
>
116141
{item.userHandle}
117142
</a>
118-
</span>
143+
</Tooltip>
119144
</td>
120145
<td className={styles.teamName}>
121-
<ProjectName projectId={item.projectId} />
146+
<Tooltip content={projectId}>
147+
<ProjectName projectId={item.projectId} />
148+
</Tooltip>
122149
</td>
123150
<td className={styles.startDate}>{item.startDate}</td>
124151
<td className={styles.endDate}>{item.endDate}</td>
@@ -175,6 +202,7 @@ PeriodItem.propTypes = {
175202
isSelected: PT.bool.isRequired,
176203
item: PT.shape({
177204
id: PT.oneOfType([PT.number, PT.string]).isRequired,
205+
jobId: PT.string.isRequired,
178206
rbId: PT.string.isRequired,
179207
projectId: PT.oneOfType([PT.number, PT.string]).isRequired,
180208
userHandle: PT.string.isRequired,

src/routes/WorkPeriods/components/PeriodItem/styles.module.scss

+18-7
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ td.toggle {
6464
@include roboto-bold;
6565
color: #0d61bf;
6666
}
67+
}
6768

68-
span {
69-
display: block;
70-
max-width: 20em;
71-
overflow: hidden;
72-
text-overflow: ellipsis;
73-
white-space: nowrap;
74-
}
69+
.userHandleContainer {
70+
display: inline-block;
71+
max-width: 20em;
72+
overflow: hidden;
73+
text-overflow: ellipsis;
74+
white-space: nowrap;
7575
}
7676

7777
td.teamName {
@@ -106,3 +106,14 @@ td.paymentTotal {
106106
td.daysWorked {
107107
padding: 5px 10px;
108108
}
109+
110+
.tooltipContent {
111+
white-space: nowrap;
112+
font-weight: 400;
113+
}
114+
115+
.tooltipLabel {
116+
margin-right: 5px;
117+
white-space: nowrap;
118+
font-weight: 500;
119+
}

0 commit comments

Comments
 (0)