Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooltip refactor #1168

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

williamsyang-work
Copy link
Contributor

@williamsyang-work williamsyang-work commented Jan 30, 2025

Refactor Tooltip Components

Overview

This PR modernizes our tooltip implementation by replacing the react-tooltip dependency with a custom tooltip component and introducing a more React-idiomatic state management approach.

Key Changes

  • Removes dependency on react-tooltip in favor of a custom implementation
  • Replaces direct DOM manipulation via refs with proper React state management
  • Introduces an abstract component pattern for consistent tooltip behavior
  • Removes the separate XY tooltip implementation in favor of a unified approach
  • Improves type safety and component reusability

Technical Details

New Tooltip Implementation

The new tooltip component uses React's built-in state management and Portal API, providing better control over positioning and animations:

export const TooltipComponent = ({ content = '⏳', visible = false, fadeTransition = 500 }: TooltipProps) => {
    const [state, setState] = useState<TooltipState>({
        content,
        visible,
        zIndex: visible ? 99999 : -99999
    });

    // Clean React-based positioning logic
    const calculateAndSetPosition = () => {
        if (!tooltipRef.current) return;
        
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        const tooltipRect = tooltipRef.current.getBoundingClientRect();
        
        // Position calculation logic
    };
}

Compared to the old implementation which relied on external library and callbacks:

<ReactTooltip
    className="react-tooltip"
    id="tooltip-component"
    effect="float"
    type="info"
    place="bottom"
    html={true}
    delayShow={500}
    delayUpdate={500}
    afterShow={() => {
        if (this.state.content === undefined) {
            this.fetchContent(this.state.element);
        }
    }}
/>

Abstract Component Pattern

Enhanced the existing AbstractOutputComponent with tooltip management capabilities:

interface AbstractOutputState {
    tooltipContent?: React.ReactNode;
    tooltipVisible?: boolean;
}

export abstract class AbstractOutputComponent<P extends AbstractOutputProps, S extends AbstractOutputState> extends React.Component<P, S> {
    render(): JSX.Element {
        return (
            <div onMouseLeave={() => this.closeTooltip()}>
                {/* Component content */}
                <TooltipComponent 
                    content={this.state.tooltipContent} 
                    visible={this.state.tooltipVisible} 
                />
            </div>
        );
    }

    protected setTooltipContent(content: React.ReactNode): void {
        this.setState({ tooltipContent: content, tooltipVisible: true });
    }

    protected closeTooltip(): void {
        this.setState({ tooltipVisible: false });
    }
}

This replaces the old callback-based approach:

setElement<T>(element: T, func?: (element: T) => MaybePromise<{ [key: string]: string } | undefined>): void {
    if (element !== this.state.element && this.state.element) {
        if (this.state.content) {
            if (this.timerId === undefined) {
                this.timerId = setTimeout(() => {
                    if (this.state.element !== element) {
                        ReactTooltip.hide();
                        this.setState({ content: undefined });
                    }
                }, 500);
            }
        }
    }
    this.setState({ element, func });
}

Removed Complexity

  • Eliminated callback-based tooltip content generation
  • Removed manual DOM event handling for positioning
  • Simplified tooltip state management

Migration Examples

Here are examples of how components are being updated to use the new tooltip pattern:

XY Chart Tooltip

// ...
if (points.length > 0) {
    this.setTooltipContent(this.generateXYComponentTooltip(tooltipData));
} else {
    this.closeTooltip();
}
// ...

private generateXYComponentTooltip = (tooltipData: TooltipData) => (
    <>
        <p style={{ margin: '0 0 5px 0' }}>{tooltipData?.title}</p>
        <ul style={{ padding: '0' }}>
            {tooltipData.dataPoints?.map(
                (point: { color: string; background: string; label: string; value: string }, i: number) => (
                    <li key={i} style={{ listStyle: 'none', display: 'flex', marginBottom: 5 }}>
                        <div
                            style={{
                                height: '10px',
                                width: '10px',
                                margin: 'auto 0',
                                border: 'solid thin',
                                borderColor: point.color,
                                backgroundColor: point.background
                            }}
                        ></div>
                        <span style={{ marginLeft: '5px' }}>
                            {point.label} {point.value}
                        </span>
                    </li>
                )
            )}
        </ul>
        {tooltipData.zeros > 0 && (
            <p style={{ marginBottom: 0 }}>
                {tooltipData.zeros} other{tooltipData.zeros > 1 ? 's' : ''}: 0
            </p>
        )}
    </>
);

Table Tooltip

// ...
const tooltipContent = this.generateTooltipTable(tooltipObject);
this.setTooltipContent(tooltipContent);
// ...

private generateTooltipTable(tooltip: { [key: string]: string }) {
    return (
        <table>
            {Object.keys(tooltip).map(key => (
                <tr key={key}>
                    <td style={{ textAlign: 'left' }}> {key} </td>
                    <td style={{ textAlign: 'left' }}> {tooltip[key]} </td>
                </tr>
            ))}
        </table>
    );
}

Testing Instructions

  • Verify tooltip positioning works correctly near viewport edges
  • Check fade animations on show/hide
  • Ensure tooltips close properly on scroll/mouse leave
  • Test with different content types (text, elements, async content)
  • Compatability with vscode-trace-extension

Future Considerations

  • Potential for additional customization options (colors, sizes, animations)
  • Performance optimizations for frequent tooltip updates
  • Accessibility improvements

@williamsyang-work williamsyang-work force-pushed the tooltip-refactor branch 9 times, most recently from 20ed423 to 73c1356 Compare February 5, 2025 21:38
@bhufmann
Copy link
Collaborator

bhufmann commented Feb 6, 2025

@williamsyang-work thanks for this update. I see that it's still a draft PR. Let us know when it's ready for review.

@williamsyang-work williamsyang-work force-pushed the tooltip-refactor branch 8 times, most recently from d9a90b9 to e2bf86e Compare February 7, 2025 17:13
@williamsyang-work
Copy link
Contributor Author

@williamsyang-work thanks for this update. I see that it's still a draft PR. Let us know when it's ready for review.

@bhufmann I just cleaned up the PR. It's ready for review!

@williamsyang-work williamsyang-work marked this pull request as ready for review February 7, 2025 17:26
private getContent() {
if (this.state.content) {
return this.state.content;
// Handle content and visibility changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure where this happens or can be fixed, but when you hover over the tooltip and do a text range selection, some range selection remains when you close and reopen the tooltip, even when the text content changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what you mean exactly... Can you post a GIF?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hover on a state on the kworker row, then mouse over the tooltip and select the End time value:
tooltip1

Then I move my mouse and hover over a state on the swapper row, and in the tooltip the End time value (a different string) is already selected:
tooltip2

This example is neat, but when the tooltip content is very different, then the persisted selection can be anything, starting and ending in the middle of words from different properties.

{Object.keys(tooltip).map(key => (
<tr key={key}>
<td style={{ textAlign: 'left' }}> {key} </td>
<td style={{ textAlign: 'left' }}> {tooltip[key]} </td>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd formatting to add spaces around the {...} ? Does it get stripped in the html?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spaces around the {...}

On lines 830-831? I usually don't put spaces around props prop={'content'} but i will inside JS objects { object: 'value' }
At this point it's JSX and not actual HTML. Some react magic converts it to HTML and I'm not sure how that works! Haha.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean around {key} and {tooltip[key]} inside the html tag.

}

export class TooltipComponent extends React.Component<unknown, TooltipComponentState> {
private static readonly HOURGLASS_NOT_DONE = '&#x23f3;';
export const TooltipComponent = ({ content = '⏳', visible = false, fadeTransition = 500 }: TooltipProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get error/warning: Missing return type on function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added typing for the function.

@@ -751,7 +770,7 @@ export abstract class AbstractXYOutputComponent<
rightPos = xLocation;
}

const tooltipData = {
const tooltipData: TooltipData = {
title: timeLabel,
dataPoints: points,
top: topPos,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four positioning fields are not used, so the position calculations above are also not needed, and the x,y function parameters are not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I went ahead and removed the function inputs (x, y) as well as the following keys from TooltipData: top, bottom, right, left and opacity, transition.

Function calls of tooltip have also been fixed in xy-output-component and xy-output-overview-component.

top?: number;
bottom?: number;
right?: number;
left?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four position fields are not used.

right?: number;
left?: number;
opacity?: number;
transition?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transition field is not used.

dependencies:
prop-types "^15.7.2"
uuid "^7.0.3"

react-tooltip@^4.2.21:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking, is this one still needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a dependency of @theia/core - Indeed, I do not see directly used in this repo here, but it might be used in the Theia examples in this repo.

$ yarn why react-tooltip
yarn why v1.22.22
[1/4] Why do we have the module "react-tooltip"...?
[2/4] Initialising dependency graph...
[3/4] Finding dependency...
[4/4] Calculating file sizes...
=> Found "react-tooltip@4.5.1"
info Reasons this module exists
   - "_project_#browser-theia-trace-example-app#@theia#core" depends on it
   - Hoisted from "_project_#browser-theia-trace-example-app#@theia#core#react-tooltip"
info Disk size without dependencies: "956KB"
info Disk size with unique dependencies: "1.48MB"
info Disk size with transitive dependencies: "1.6MB"
info Number of shared dependencies: 6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did check this out before submitting this PR. I made sure react-tooltip was removed from all of the codebase as well as all package.json files. I came to the same conclusion as @marcdumais-work, that @theia was using it somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, this dependency will not be part of the published npm packages (since they do not depend on it), so we should be good.

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>
@williamsyang-work
Copy link
Contributor Author

@PatrickTasse sorry for the delay. I addressed all of your comments except the first one, which I need some clarification on. Thank you!

@williamsyang-work
Copy link
Contributor Author

williamsyang-work commented Mar 5, 2025

After some thinking I decided to refactor this into a React Class Component. It will be easier to maintain.

In calculateAndSetPosition I also moved the div's position manipulation from direct css editing to a react state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants