Skip to content

Commit dbf1dcf

Browse files
Tooltip refactor
Simplifies the rendering of tooltips inside of all available views. Fixes some UI bugs with tooltips. Signed-off-by: Will Yang <william.yang@ericsson.com>
1 parent b39c595 commit dbf1dcf

12 files changed

+190
-350
lines changed

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,91 @@
1+
import { render, screen } 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;
5+
// Mock ReactTooltip since we don't need its actual implementation for these tests
6+
jest.mock('react-tooltip', () => {
7+
return function MockReactTooltip(props: any) {
8+
return (
9+
<div
10+
data-testid="mock-tooltip"
11+
id={props.id}
12+
className="__react_component_tooltip"
13+
data-type={props.type}
14+
data-effect={props.effect}
15+
data-place={props.place}
16+
data-clickable={props.clickable}
17+
data-scroll-hide={props.scrollHide}
18+
>
19+
{props.children}
20+
</div>
21+
);
1722
};
23+
});
1824

19-
beforeEach(() => {
20-
axisComponent = null;
25+
describe('TooltipComponent', () => {
26+
it('renders itself', () => {
27+
const { container } = render(<TooltipComponent visible={true} />);
28+
expect(screen.getByTestId('mock-tooltip')).toBeTruthy();
2129
});
2230

23-
afterEach(() => {
24-
cleanup();
25-
jest.clearAllMocks();
31+
it('renders an hourglass when visible with no content', () => {
32+
const { container } = render(<TooltipComponent visible={true} />);
33+
const tooltip = screen.getByTestId('mock-tooltip');
34+
expect(tooltip.textContent).toBe('⏳');
2635
});
2736

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);
37+
it('renders the provided content if visible with content', () => {
38+
const testContent = <div>Test Content</div>;
39+
const { container } = render(<TooltipComponent visible={true} content={testContent} />);
40+
const tooltip = screen.getByTestId('mock-tooltip');
41+
expect(tooltip.textContent).toBe('Test Content');
4042
});
4143

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);
44+
it('does not render when not visible', () => {
45+
const { container } = render(<TooltipComponent visible={false} content="Test Content" />);
46+
const tooltip = screen.queryByTestId('mock-tooltip');
47+
expect(tooltip).toBeNull();
48+
});
5349

54-
expect(tooltip.setState).toBeCalledWith({ content: undefined });
50+
it('has the correct tooltip ID', () => {
51+
const { container } = render(<TooltipComponent visible={true} />);
52+
const tooltip = screen.getByTestId('mock-tooltip');
53+
expect(tooltip.id).toBe('tooltip-component');
5554
});
5655

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);
56+
it('applies correct tooltip configuration', () => {
57+
const { container } = render(<TooltipComponent visible={true} />);
58+
const tooltip = screen.getByTestId('mock-tooltip');
6459

65-
expect(tooltip.setState).toBeCalledWith({ element: model, func: undefined });
60+
expect(tooltip.getAttribute('data-type')).toBe('info');
61+
expect(tooltip.getAttribute('data-effect')).toBe('float');
62+
expect(tooltip.getAttribute('data-place')).toBe('bottom');
63+
expect(tooltip.getAttribute('data-clickable')).toBe('true');
64+
expect(tooltip.getAttribute('data-scroll-hide')).toBe('true');
6665
});
6766

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 });
67+
it('renders complex React nodes as content', () => {
68+
const complexContent = (
69+
<div>
70+
<h1>Title</h1>
71+
<p>Description</p>
72+
<span>Details</span>
73+
</div>
74+
);
75+
const { container } = render(<TooltipComponent visible={true} content={complexContent} />);
76+
const tooltip = screen.getByTestId('mock-tooltip');
77+
expect(tooltip.textContent).toBe('TitleDescriptionDetails');
7878
});
7979

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);
80+
it('renders null content as hourglass', () => {
81+
const { container } = render(<TooltipComponent visible={true} content={null} />);
82+
const tooltip = screen.getByTestId('mock-tooltip');
83+
expect(tooltip.textContent).toBe('⏳');
84+
});
8785

88-
expect(tooltip.setState).toBeCalledWith({ content: undefined });
86+
it('renders undefined content as hourglass', () => {
87+
const { container } = render(<TooltipComponent visible={true} content={undefined} />);
88+
const tooltip = screen.getByTestId('mock-tooltip');
89+
expect(tooltip.textContent).toBe('⏳');
8990
});
9091
});

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

+12-3
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,6 +98,7 @@ export abstract class AbstractOutputComponent<
9998
onMouseDown={this.props.onMouseDown}
10099
onTouchStart={this.props.onTouchStart}
101100
onTouchEnd={this.props.onTouchEnd}
101+
onMouseLeave={() => this.closeTooltip()}
102102
data-tip=""
103103
data-for="tooltip-component"
104104
>
@@ -117,6 +117,7 @@ export abstract class AbstractOutputComponent<
117117
{this.renderMainOutputContainer()}
118118
</div>
119119
{this.props.children}
120+
<TooltipComponent content={this.state.tooltipContent} visible={this.state.tooltipVisible} />
120121
</div>
121122
);
122123
}
@@ -314,4 +315,12 @@ export abstract class AbstractOutputComponent<
314315
protected getTitleBarTooltip(): string {
315316
return this.props.outputDescriptor.name;
316317
}
318+
319+
protected setTooltipContent(content: React.ReactNode): void {
320+
this.setState({ tooltipContent: content, tooltipVisible: true });
321+
}
322+
323+
protected closeTooltip(): void {
324+
this.setState({ tooltipContent: undefined, tooltipVisible: false });
325+
}
317326
}

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)