Skip to content

Commit f1c9f4c

Browse files
authored
Merge pull request #21 from oslabs-beta/eva/tooltip
tooltip features incorporated
2 parents aeea7d1 + fd687ff commit f1c9f4c

File tree

5 files changed

+257
-16
lines changed

5 files changed

+257
-16
lines changed

src/app/FrontendTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface TooltipData {
5454
x: number;
5555
y: number;
5656
color: string;
57+
name?: string;
5758
}
5859

5960
export interface snapshot {

src/app/components/StateRoute/AxMap/Ax.tsx

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33
import { Group } from '@visx/group';
44
import { hierarchy, Tree } from '@visx/hierarchy';
55
import { LinearGradient } from '@visx/gradient';
66
import { pointRadial } from 'd3-shape';
7-
import { useTooltipInPortal } from '@visx/tooltip';
87
import LinkControls from './axLinkControls';
98
import getLinkComponent from './getAxLinkComponents';
9+
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
10+
import ToolTipDataDisplay from './ToolTipDataDisplay';
11+
import { ToolTipStyles } from '../../../FrontendTypes';
12+
import { localPoint } from '@visx/event';
1013
import AxLegend from './axLegend';
1114
import { renderAxLegend } from '../../../slices/AxSlices/axLegendSlice';
1215
import type { RootState } from '../../../store';
1316

17+
//still using below themes?
1418
const theme = {
1519
scheme: 'monokai',
1620
author: 'wimer hazenberg (http://www.monokai.nl)',
@@ -165,6 +169,50 @@ export default function AxTree(props) {
165169
let totalWidth = width;
166170
let totalHeight = height;
167171

172+
173+
174+
const toolTipTimeoutID = useRef(null); //useRef stores stateful data that’s not needed for rendering.
175+
const {
176+
tooltipData, // value/data that tooltip may need to render
177+
tooltipLeft, // number used for tooltip positioning
178+
tooltipTop, // number used for tooltip positioning
179+
tooltipOpen, // boolean whether the tooltip state is open or closed
180+
showTooltip, // function to set tooltip state
181+
hideTooltip, // function to close a tooltip
182+
} = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component
183+
console.log('tool tip data: ', tooltipData);
184+
// let nameVal = JSON.stringify(tooltipData)
185+
// console.log('nameVal', nameVal);
186+
const {
187+
containerRef, // Access to the container's bounding box. This will be empty on first render.
188+
TooltipInPortal, // TooltipWithBounds in a Portal, outside of your component DOM tree
189+
} = useTooltipInPortal({
190+
// Visx hook
191+
detectBounds: true, // use TooltipWithBounds
192+
scroll: true, // when tooltip containers are scrolled, this will correctly update the Tooltip position
193+
});
194+
195+
const tooltipStyles: ToolTipStyles = {
196+
...defaultStyles,
197+
minWidth: 60,
198+
maxWidth: 300,
199+
backgroundColor: 'rgb(15,15,15)',
200+
color: 'white',
201+
fontSize: '16px',
202+
lineHeight: '18px',
203+
fontFamily: 'Roboto',
204+
zIndex: 100,
205+
pointerEvents: 'all !important',
206+
};
207+
208+
// const formatRenderTime = (time: number): string => {
209+
// if (!time) return 'No time information';
210+
// const renderTime = time.toFixed(3);
211+
// return `${renderTime} ms `;
212+
// };
213+
214+
215+
168216
const [layout, setLayout] = useState('cartesian');
169217
const [orientation, setOrientation] = useState('horizontal');
170218
const [linkType, setLinkType] = useState('diagonal');
@@ -270,14 +318,6 @@ export default function AxTree(props) {
270318
populateNodeAxArr(rootAxNode);
271319
console.log('nodeAxArr: ', nodeAxArr);
272320

273-
const {
274-
containerRef // Access to the container's bounding box. This will be empty on first render.
275-
} = useTooltipInPortal({
276-
// Visx hook
277-
detectBounds: true, // use TooltipWithBounds
278-
scroll: true, // when tooltip containers are scrolled, this will correctly update the Tooltip position
279-
});
280-
281321
// ax Legend
282322
const { axLegendButtonClicked } = useSelector((state: RootState) => state.axLegend);
283323
const dispatch = useDispatch();
@@ -305,6 +345,14 @@ export default function AxTree(props) {
305345
<svg ref={containerRef} width={totalWidth + 0.2*totalWidth} height={totalHeight}>
306346
<LinearGradient id='root-gradient' from='#488689' to='#3c6e71' />
307347
<LinearGradient id='parent-gradient' from='#488689' to='#3c6e71' />
348+
<rect
349+
className='componentMapContainer'
350+
width={sizeWidth / aspect}
351+
height={sizeHeight / aspect + 0}
352+
rx={14}
353+
onClick={() => {
354+
hideTooltip();
355+
}}/>
308356
<Group transform={`scale(${aspect})`} top={margin.top} left={margin.left}>
309357
<Tree
310358
root={hierarchy(nodeAxArr[0], (d) => (d.isExpanded ? null : d.children))}
@@ -418,6 +466,19 @@ export default function AxTree(props) {
418466
} else {
419467
aspect = Math.max(aspect, 0.2);
420468
}
469+
const handleMouseAndClickOver = (event): void => {
470+
const coords = localPoint(event.target.ownerSVGElement, event);
471+
const tooltipObj = { ...node.data };
472+
console.log("tooltipobj: ", tooltipObj);
473+
474+
showTooltip({
475+
tooltipLeft: coords.x,
476+
tooltipTop: coords.y,
477+
tooltipData: tooltipObj,
478+
// this is where the data for state and render time is displayed
479+
// but does not show props functions and etc
480+
});
481+
};
421482

422483
return (
423484
<Group top={top} left={left} key={key} className='rect'>
@@ -434,6 +495,7 @@ export default function AxTree(props) {
434495
rx={node.children ? 4 : 10}
435496
onClick={() => {
436497
node.data.isExpanded = !node.data.isExpanded;
498+
hideTooltip();
437499
}}
438500
/>
439501
)}
@@ -457,6 +519,40 @@ export default function AxTree(props) {
457519
rx={node.children ? 4 : 10}
458520
onClick={() => {
459521
node.data.isExpanded = !node.data.isExpanded;
522+
hideTooltip();
523+
}}
524+
// Mouse Enter Rect (Component Node) -----------------------------------------------------------------------
525+
/** This onMouseEnter event fires when the mouse first moves/hovers over a component node.
526+
* The supplied event listener callback produces a Tooltip element for the current node. */
527+
528+
onMouseEnter={(event) => {
529+
/** This 'if' statement block checks to see if you've just left another component node
530+
* by seeing if there's a current setTimeout waiting to close that component node's
531+
* tooltip (see onMouseLeave immediately below). If so it clears the tooltip generated
532+
* from that component node so a new tooltip for the node you've just entered can render. */
533+
if (toolTipTimeoutID.current !== null) {
534+
clearTimeout(toolTipTimeoutID.current);
535+
hideTooltip();
536+
}
537+
// Removes the previous timeoutID to avoid errors
538+
toolTipTimeoutID.current = null;
539+
//This generates a tooltip for the component node the mouse has entered.
540+
handleMouseAndClickOver(event);
541+
}}
542+
// Mouse Leave Rect (Component Node) --------------------------------------------------------------------------
543+
/** This onMouseLeave event fires when the mouse leaves a component node.
544+
* The supplied event listener callback generates a setTimeout call which gives the
545+
* mouse a certain amount of time between leaving the current component node and
546+
* closing the tooltip for that node.
547+
* If the mouse enters the tooltip before the timeout delay has passed, the
548+
* setTimeout event will be canceled. */
549+
onMouseLeave={() => {
550+
// Store setTimeout ID so timeout can be cleared if necessary
551+
toolTipTimeoutID.current = setTimeout(() => {
552+
// hideTooltip unmounts the tooltip
553+
hideTooltip();
554+
toolTipTimeoutID.current = null;
555+
}, 300);
460556
}}
461557
/>
462558
)}
@@ -485,6 +581,37 @@ export default function AxTree(props) {
485581
</Tree>
486582
</Group>
487583
</svg>
584+
{tooltipOpen && tooltipData && (
585+
<TooltipInPortal
586+
// set this to random so it correctly updates with parent bounds
587+
key={Math.random()}
588+
top={tooltipTop}
589+
left={tooltipLeft}
590+
style={tooltipStyles}
591+
//------------- Mouse Over TooltipInPortal--------------------------------------------------------------------
592+
/** After the mouse enters the tooltip, it's able to persist by clearing the setTimeout
593+
* that would've unmounted it */
594+
onMouseEnter={() => {
595+
clearTimeout(toolTipTimeoutID.current);
596+
toolTipTimeoutID.current = null;
597+
}}
598+
//------------- Mouse Leave TooltipInPortal -----------------------------------------------------------------
599+
/** When the mouse leaves the tooltip, the tooltip unmounts */
600+
onMouseLeave={() => {
601+
hideTooltip();
602+
}}
603+
>
604+
<div>
605+
<div>
606+
<strong>{JSON.stringify(tooltipData['name'].value)}</strong>
607+
</div>
608+
<div>
609+
<ToolTipDataDisplay containerName='Ax Node Info' dataObj={tooltipData} />
610+
{/* <ToolTipDataDisplay containerName='State'dataObj={tooltipData}/> */}
611+
</div>
612+
</div>
613+
</TooltipInPortal>
614+
)}
488615

489616
{/* ax Legend */}
490617
<div>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { JSONTree } from 'react-json-tree';
3+
4+
/*
5+
Code that show's the tooltip of our JSON tree
6+
*/
7+
8+
const colors = {
9+
scheme: 'paraiso',
10+
author: 'jan t. sott',
11+
base00: '#2f1e2e',
12+
base01: '#41323f',
13+
base02: '#4f424c',
14+
base03: '#776e71',
15+
base04: '#8d8687',
16+
base05: '#a39e9b',
17+
base06: '#b9b6b0',
18+
base07: '#e7e9db',
19+
base08: '#ef6155',
20+
base09: '#824508', //base09 is orange for booleans and numbers. This base in particular fails to match the entered color.
21+
// base09: '#592bad', // alternative purple
22+
base0A: '#fec418',
23+
base0B: '#48b685',
24+
base0C: '#5bc4bf',
25+
base0D: '#06b6ef',
26+
base0E: '#815ba4',
27+
base0F: '#e96ba8',
28+
};
29+
30+
const ToolTipDataDisplay = ({ containerName, dataObj }) => {
31+
const printableObject = {}; // The key:value properties of printableObject will be rendered in the JSON Tree
32+
33+
if (!dataObj) {
34+
// If state is null rather than an object, print "State: null" in tooltip
35+
printableObject[containerName] = dataObj;
36+
} else {
37+
/*
38+
Props often contain circular references.
39+
Messages from the backend must be sent as JSON objects (strings).
40+
JSON objects can't contain circular ref's, so the backend filters out problematic values by stringifying the values of object properties and ignoring any values that fail the conversion due to a circular ref. The following logic converts these values back to JS so they display clearly and are collapsible.
41+
*/
42+
const data = {};
43+
//ignored false vs true
44+
//ignored reasons here
45+
//&& key = name? / order?
46+
for (const key in dataObj) {
47+
console.log('keys in dataObj in tooltiptotalDisplay: ', key);
48+
if(key === 'properties' || key === 'ignored' || key === 'ignoredReasons'){// loop through properties, adding them to the data object
49+
50+
if (typeof dataObj[key] === 'string') {
51+
//if 'key' is ignored, put the ignored key and its value on the data object
52+
//if ignoredReasons has length it should loop through adding the reasons names to the data object
53+
//actually might only need to give it the properties and ignored and ignored reasons and it'll take care of the rest
54+
try {
55+
data[key] = JSON.parse(dataObj[key]);
56+
} catch {
57+
data[key] = dataObj[key];
58+
}
59+
} else {
60+
data[key] = dataObj[key];
61+
}
62+
}
63+
}
64+
/*
65+
Adds container name (State, Props, future different names for hooks) at top of object so everything nested in it will collapse when you click on it.
66+
*/
67+
printableObject[containerName] = data;
68+
}
69+
70+
return (
71+
<div className='tooltipData' key={`${containerName}-data-container`}>
72+
<JSONTree
73+
data={printableObject} // data to be rendered, a snapshot object
74+
theme={{ extend: colors, tree: () => ({ className: `tooltipData-JSONTree` }) }} // theme set to a base16 theme that has been extended to include "className: 'json-tree'"
75+
shouldExpandNodeInitially={() => true} // determines if node should be expanded when it first renders (root is expanded by default)
76+
hideRoot={true} // hides the root node
77+
/>
78+
</div>
79+
);
80+
};
81+
82+
export default ToolTipDataDisplay;

src/app/components/StateRoute/ComponentMap/ComponentMap.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export default function ComponentMap({
109109
hideTooltip, // function to close a tooltip
110110
} = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component
111111

112+
console.log('tooltipData component map: ', tooltipData);
112113
const {
113114
containerRef, // Access to the container's bounding box. This will be empty on first render.
114115
TooltipInPortal, // TooltipWithBounds in a Portal, outside of your component DOM tree

src/extension/background.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ const metrics = {};
1717
// function pruning the chrome ax tree and pulling the relevant properties
1818
const pruneAxTree = (axTree) => {
1919
const axArr = [];
20+
let orderCounter = 0
21+
2022
for (const node of axTree) {
23+
2124
let {
2225
backendDOMNodeId,
2326
childIds,
@@ -30,34 +33,61 @@ const pruneAxTree = (axTree) => {
3033
role
3134
} = node;
3235

36+
// let order;
37+
3338
if(!name){
3439
if(ignored){
35-
name = {value: `ignored node: ${ignoredReasons[0].name}`};
40+
name = {value: 'ignored node'};
3641
}
3742
else{
38-
name = {value: 'visible node with no name'};
43+
name = {value: 'no name'};
3944
}
4045
}
4146
if(!name.value){
42-
name.value = 'visible node with no name';
47+
name.value = 'no name';
4348
}
44-
49+
//if the node is ignored, it should be given an order number as it won't be read at all
50+
// if(ignored){
51+
// order = null;
52+
// }
53+
// else{
54+
// order = orderCounter++;
55+
// }
4556
if (role.type === 'role') {
4657
const axNode = {
4758
backendDOMNodeId: backendDOMNodeId,
4859
childIds: childIds,
4960
ignored: ignored,
61+
ignoredReasons: ignoredReasons,
5062
name: name,
5163
nodeId: nodeId,
5264
ignoredReasons: ignoredReasons,
5365
parentId: parentId,
5466
properties: properties,
67+
// order: order,
5568
};
56-
5769
axArr.push(axNode);
5870
}
5971
}
60-
console.log('axArr: ', axArr);
72+
73+
// Sort nodes by backendDOMNodeId in ascending order
74+
//try with deep copy
75+
// //aria properties
76+
// console.log('axArr before : ', axArr);
77+
// axArr.sort((a, b) => b.backendDOMNodeId - a.backendDOMNodeId);
78+
79+
// Assign order based on sorted position
80+
for (const axNode of axArr) {
81+
// console.log('current axnode order number: ', axNode.order)
82+
// console.log('this iterations node', axNode);
83+
if (!axNode.ignored) { // Assuming you only want to assign order to non-ignored nodes
84+
axNode.order = orderCounter++;
85+
} else {
86+
axNode.order = null; // Or keep it undefined, based on your requirement
87+
}
88+
// console.log('current axnode order number: ', axNode.order)
89+
}
90+
// console.log('axArr after: ', axArr);
6191
return axArr;
6292
};
6393

0 commit comments

Comments
 (0)