Skip to content

Commit d9a90b9

Browse files
Tooltip refactor
Removes 'react-tooltip' dependency and adds an in-house tooltip impl. Simplifies the rendering of tooltips inside of all available views. Fixes some UI bugs with tooltips. Fixes the bugged hourglass bug. Signed-off-by: Will Yang <william.yang@ericsson.com>
1 parent b39c595 commit d9a90b9

16 files changed

+299
-380
lines changed

packages/react-components/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"react-contexify": "^5.0.0",
3434
"react-grid-layout": "1.2.0",
3535
"react-modal": "^3.8.1",
36-
"react-tooltip": "4.2.14",
3736
"react-virtualized": "^9.21.0",
3837
"timeline-chart": "^0.4.1",
3938
"traceviewer-base": "0.6.0",

packages/react-components/src/components/__tests__/__snapshots__/table-renderer-components.test.tsx.snap

-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ exports[`<TableOutputComponent /> Renders AG-Grid table with provided props & st
7878
<div>
7979
<div
8080
class="output-container undefined"
81-
data-for="tooltip-component"
82-
data-tip=""
8381
id="00"
8482
style="width: 0px; height: 0px;"
8583
tabindex="-1"

packages/react-components/src/components/__tests__/table-renderer-components.test.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ describe('<TableOutputComponent />', () => {
2727

2828
test('Renders AG-Grid table with provided props & state', async () => {
2929
const tableOutputComponentProps: AbstractOutputProps = {
30-
tooltipComponent: null,
3130
style: {
3231
width: 0,
3332
height: 0,
@@ -61,7 +60,6 @@ describe('<TableOutputComponent />', () => {
6160
onOutputRemove: () => 0,
6261
unitController: new TimeGraphUnitController(BigInt(0), { start: BigInt(0), end: BigInt(0) }),
6362
backgroundTheme: 'light',
64-
tooltipXYComponent: null,
6563
outputWidth: 0
6664
};
6765

@@ -95,7 +93,6 @@ describe('<TableOutputComponent />', () => {
9593
// Renders with provided props
9694
expect(table.state.tableColumns).toEqual(tableOutputComponentState);
9795
expect(table.props.backgroundTheme).toEqual('light');
98-
expect(table.props.tooltipComponent).toEqual(null);
9996
expect(table.props.style).toEqual({
10097
width: 0,
10198
height: 0,
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,36 @@
1+
import { render, fireEvent, act } from '@testing-library/react';
12
import * as React from 'react';
2-
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
33
import { TooltipComponent } from '../tooltip-component';
44

5-
const model = {
6-
id: '123',
7-
range: { start: 1, end: 10 },
8-
label: 'model'
9-
};
10-
const tooltip = new TooltipComponent(10);
11-
tooltip.setState = jest.fn();
12-
13-
describe('Tooltip component', () => {
14-
let axisComponent: any;
15-
const ref = (el: TooltipComponent | undefined | null): void => {
16-
axisComponent = el;
17-
};
18-
5+
describe('TooltipComponent', () => {
196
beforeEach(() => {
20-
axisComponent = null;
21-
});
22-
23-
afterEach(() => {
24-
cleanup();
25-
jest.clearAllMocks();
7+
document.body.innerHTML = '';
268
});
279

28-
/*
29-
* Skip due to issues with TooltipComponent:
30-
*
31-
* react-tooltip v4.2.14 works in the application but causes an exception
32-
* in tests (https://github.com/ReactTooltip/react-tooltip/issues/681),
33-
* which is fixed in v4.2.17. However, version v4.2.17 breaks the tooltip
34-
* when running in the application.
35-
*/
36-
it.skip('renders itself', () => {
37-
render(<TooltipComponent />);
38-
expect(axisComponent).toBeTruthy();
39-
expect(axisComponent instanceof TooltipComponent).toBe(true);
10+
it('renders content', async () => {
11+
render(<TooltipComponent visible={true} content="Test Content" />);
12+
const tooltip = document.querySelector('.trace-compass-tooltip') as HTMLElement;
13+
expect(tooltip.textContent).toBe('Test Content');
14+
expect(tooltip.style.opacity).toBe('1');
4015
});
4116

42-
// Skip due to issues with TooltipComponent (see above for details)
43-
it.skip('resets timer on mouse enter', () => {
44-
tooltip.state = {
45-
element: model,
46-
func: undefined,
47-
content: 'Test'
48-
};
49-
render(<TooltipComponent />);
50-
const component = screen.getByRole('tooltip-component-role');
51-
fireEvent.mouseEnter(component);
52-
fireEvent.mouseLeave(component);
53-
54-
expect(tooltip.setState).toBeCalledWith({ content: undefined });
55-
});
56-
57-
it('displays a tooltip for a time graph state component', () => {
58-
tooltip.state = {
59-
element: undefined,
60-
func: undefined,
61-
content: undefined
62-
};
63-
tooltip.setElement(model);
64-
65-
expect(tooltip.setState).toBeCalledWith({ element: model, func: undefined });
66-
});
67-
68-
it('hides tooltip if mouse is not hovering over element', async () => {
69-
tooltip.state = {
70-
element: model,
71-
func: undefined,
72-
content: 'Test'
73-
};
74-
tooltip.setElement(undefined);
75-
await new Promise(r => setTimeout(r, 500));
76-
77-
expect(tooltip.setState).toBeCalledWith({ content: undefined });
17+
it('de-renders when visibility changes', async () => {
18+
const { rerender } = render(<TooltipComponent visible={true} content="Test" />);
19+
const tooltip = document.querySelector('.trace-compass-tooltip') as HTMLElement;
20+
21+
expect(tooltip.style.opacity).toBe('1');
22+
23+
rerender(<TooltipComponent visible={false} content="Test" />);
24+
await new Promise(resolve => setTimeout(resolve, 1001));
25+
expect(tooltip.style.opacity).toBe('0');
7826
});
7927

80-
it('hide tooltip because there is no content', () => {
81-
tooltip.state = {
82-
element: model,
83-
func: undefined,
84-
content: undefined
85-
};
86-
tooltip.setElement(undefined);
28+
it('stays rendered when mouse moves on top of it', async () => {
29+
render(<TooltipComponent visible={true} content="Test" />);
30+
const tooltip = document.querySelector('.trace-compass-tooltip') as HTMLElement;
8731

88-
expect(tooltip.setState).toBeCalledWith({ content: undefined });
32+
fireEvent.mouseEnter(tooltip);
33+
await new Promise(resolve => setTimeout(resolve, 500));
34+
expect(tooltip.style.opacity).toBe('1');
8935
});
90-
});
36+
});

packages/react-components/src/components/abstract-output-component.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@ import { TimeRange } from 'traceviewer-base/lib/utils/time-range';
88
import { OutputComponentStyle } from './utils/output-component-style';
99
import { OutputStyleModel } from 'tsp-typescript-client/lib/models/styles';
1010
import { TooltipComponent } from './tooltip-component';
11-
import { TooltipXYComponent } from './tooltip-xy-component';
1211
import { ResponseStatus } from 'tsp-typescript-client/lib/models/response/responses';
1312
import { signalManager } from 'traceviewer-base/lib/signals/signal-manager';
1413
import { DropDownComponent, DropDownSubSection, OptionState } from './drop-down-component';
1514

1615
export interface AbstractOutputProps {
1716
tspClient: ITspClient;
18-
tooltipComponent: TooltipComponent | null;
19-
tooltipXYComponent: TooltipXYComponent | null;
2017
traceId: string;
2118
traceName?: string;
2219
range: TimeRange;
@@ -51,6 +48,8 @@ export interface AbstractOutputState {
5148
outputStatus: string;
5249
styleModel?: OutputStyleModel;
5350
dropDownOpen?: boolean;
51+
tooltipContent?: React.ReactNode;
52+
tooltipVisible?: boolean;
5453
}
5554

5655
export abstract class AbstractOutputComponent<
@@ -99,8 +98,7 @@ export abstract class AbstractOutputComponent<
9998
onMouseDown={this.props.onMouseDown}
10099
onTouchStart={this.props.onTouchStart}
101100
onTouchEnd={this.props.onTouchEnd}
102-
data-tip=""
103-
data-for="tooltip-component"
101+
onMouseLeave={() => this.closeTooltip()}
104102
>
105103
<div
106104
id={this.getOutputComponentDomId() + 'handle'}
@@ -117,6 +115,7 @@ export abstract class AbstractOutputComponent<
117115
{this.renderMainOutputContainer()}
118116
</div>
119117
{this.props.children}
118+
<TooltipComponent content={this.state.tooltipContent} visible={this.state.tooltipVisible} />
120119
</div>
121120
);
122121
}
@@ -314,4 +313,12 @@ export abstract class AbstractOutputComponent<
314313
protected getTitleBarTooltip(): string {
315314
return this.props.outputDescriptor.name;
316315
}
316+
317+
protected setTooltipContent(content: React.ReactNode): void {
318+
this.setState({ tooltipContent: content, tooltipVisible: true });
319+
}
320+
321+
protected closeTooltip(): void {
322+
this.setState({ tooltipVisible: false });
323+
}
317324
}

packages/react-components/src/components/abstract-xy-output-component.tsx

+55-10
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ export type AbstractXYOutputState = AbstractTreeOutputState & {
6464
cursor?: string;
6565
};
6666

67+
interface Point {
68+
label: any;
69+
color: any;
70+
background: any;
71+
value: string;
72+
}
73+
74+
interface TooltipData {
75+
title?: string;
76+
dataPoints?: Point[];
77+
top?: number;
78+
bottom?: number;
79+
right?: number;
80+
left?: number;
81+
opacity?: number;
82+
transition?: string;
83+
zeros: number;
84+
}
85+
6786
class xyPair {
6887
x: number;
6988
y: number;
@@ -664,7 +683,7 @@ export abstract class AbstractXYOutputComponent<
664683
: this.chartRef.current.chartInstance.height;
665684
const arraySize = this.state.xyData.labels.length;
666685
const index = Math.max(Math.round((xPos / chartWidth) * (arraySize - 1)), 0);
667-
const points: any = [];
686+
const points: Point[] = [];
668687
let zeros = 0;
669688

670689
this.state.xyData.datasets.forEach((d: any) => {
@@ -717,7 +736,7 @@ export abstract class AbstractXYOutputComponent<
717736
// If there are more than 10 lines in the chart, summarise the ones that are equal to 0.
718737
if (!invalidPoint) {
719738
if (this.state.xyData.datasets.length <= 10 || rounded > 0) {
720-
const point: any = {
739+
const point = {
721740
label: d.label,
722741
color: d.borderColor,
723742
background: d.backgroundColor,
@@ -751,7 +770,7 @@ export abstract class AbstractXYOutputComponent<
751770
rightPos = xLocation;
752771
}
753772

754-
const tooltipData = {
773+
const tooltipData: TooltipData = {
755774
title: timeLabel,
756775
dataPoints: points,
757776
top: topPos,
@@ -763,15 +782,41 @@ export abstract class AbstractXYOutputComponent<
763782
};
764783

765784
if (points.length > 0) {
766-
this.props.tooltipXYComponent?.setElement(tooltipData);
785+
this.setTooltipContent(this.generateXYComponentTooltip(tooltipData));
767786
} else {
768-
this.hideTooltip();
787+
this.closeTooltip();
769788
}
770789
}
771790

772-
protected hideTooltip(): void {
773-
this.props.tooltipXYComponent?.setElement({
774-
opacity: 0
775-
});
776-
}
791+
private generateXYComponentTooltip = (tooltipData: TooltipData) => (
792+
<>
793+
<p style={{ margin: '0 0 5px 0' }}>{tooltipData?.title}</p>
794+
<ul style={{ padding: '0' }}>
795+
{tooltipData.dataPoints?.map(
796+
(point: { color: string; background: string; label: string; value: string }, i: number) => (
797+
<li key={i} style={{ listStyle: 'none', display: 'flex', marginBottom: 5 }}>
798+
<div
799+
style={{
800+
height: '10px',
801+
width: '10px',
802+
margin: 'auto 0',
803+
border: 'solid thin',
804+
borderColor: point.color,
805+
backgroundColor: point.background
806+
}}
807+
></div>
808+
<span style={{ marginLeft: '5px' }}>
809+
{point.label} {point.value}
810+
</span>
811+
</li>
812+
)
813+
)}
814+
</ul>
815+
{tooltipData.zeros > 0 && (
816+
<p style={{ marginBottom: 0 }}>
817+
{tooltipData.zeros} other{tooltipData.zeros > 1 ? 's' : ''}: 0
818+
</p>
819+
)}
820+
</>
821+
);
777822
}

packages/react-components/src/components/timegraph-output-component.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,17 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
214214
this.onElementSelected(this.selectedElement);
215215
});
216216
this.chartLayer.registerMouseInteractions({
217-
mouseover: el => {
218-
this.props.tooltipComponent?.setElement(el, () => this.fetchTooltip(el));
217+
mouseover: async el => {
218+
const tooltipObject = await this.fetchTooltip(el);
219+
if (!tooltipObject) {
220+
this.closeTooltip();
221+
} else {
222+
const tooltipContent = this.generateTooltipTable(tooltipObject);
223+
this.setTooltipContent(tooltipContent);
224+
}
219225
},
220226
mouseout: () => {
221-
this.props.tooltipComponent?.setElement(undefined);
227+
this.closeTooltip();
222228
},
223229
click: (el, ev, clickCount) => {
224230
if (clickCount === 2) {
@@ -816,6 +822,19 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent<Timegr
816822
return fullTooltip;
817823
}
818824

825+
private generateTooltipTable(tooltip: { [key: string]: string }) {
826+
return (
827+
<table>
828+
{Object.keys(tooltip).map(key => (
829+
<tr key={key}>
830+
<td style={{ textAlign: 'left' }}> {key} </td>
831+
<td style={{ textAlign: 'left' }}> {tooltip[key]} </td>
832+
</tr>
833+
))}
834+
</table>
835+
);
836+
}
837+
819838
private renderTimeGraphContent() {
820839
return (
821840
<div id="main-timegraph-content" ref={this.horizontalContainer} style={{ height: this.props.style.height }}>

0 commit comments

Comments
 (0)