Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Feature/tooltip closing#6007

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
JoaquinPalacios wants to merge23 commits intorecharts:main
base:main
Choose a base branch
Loading
fromJoaquinPalacios:feature/tooltip-closing

Conversation

JoaquinPalacios
Copy link
Contributor

Implement tooltip close on outside click for trigger="click"

Description

This PR implements the ability to close click-triggered tooltips by clicking outside the chart area. Previously, tooltips with trigger="click" would remain open until the user clicked on another chart element or manually closed them. Now, clicking anywhere outside the chart will close the tooltip.

Related Issue

#3573

Motivation and Context

Users expect click-triggered tooltips to behave like standard UI tooltips that close when clicking outside their area. This is a common UX pattern that improves usability and provides a more intuitive interaction model for charts with click-triggered tooltips.

How Has This Been Tested?

  • Unit Tests: Added comprehensive tests in tooltipEventType.spec.tsx to verify outside click behavior for both axis-based and item-based tooltips
  • Integration Tests: Verified that the feature works across all chart types (Bar, Pie, Scatter, Funnel, RadialBar, Treemap)
  • Regression Testing: Ensured existing tooltip functionality remains intact for hover-triggered tooltips
  • Edge Cases: Tested scenarios with multiple charts, external event handlers, and various tooltip configurations

Test Environment:

  • Node.js environment with Jest test runner
  • All existing tooltip tests continue to pass
  • New outside click tests specifically validate the new functionality

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • I have added a storybook story or extended an existing story to show my changes

Note: The implementation includes:

  • Redux actions for tooltip closing (tooltipCloseStart, tooltipCloseEnd, clearClickTooltip)
  • Outside click detection in RechartsWrapper component
  • Updated event handler logic in chart components to properly forward external handlers
  • Fixed Treemap rendering issue that was discovered during testing
  • Comprehensive test coverage for the new functionality
  • The changes are backward compatible and only affect click-triggered tooltips, leaving hover-triggered tooltips unchanged.

agrummer reacted with heart emoji
@codecovCodecov
Copy link

codecovbot commentedJun 25, 2025
edited
Loading

Bundle Report

Changes will increase total bundle size by 8.62kB (0.37%) ⬆️. This is within theconfigured threshold ✅

Detailed changes
Bundle nameSizeChange
recharts/bundle-cjs1.01MB8.62kB (0.86%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset NameSize ChangeTotal SizeChange (%)
chart/Treemap.js825 bytes28.65kB2.96%
polar/Pie.js258 bytes23.97kB1.09%
cartesian/Bar.js186 bytes23.38kB0.8%
cartesian/Scatter.js231 bytes21.25kB1.1%
cartesian/Funnel.js211 bytes18.67kB1.14%
polar/RadialBar.js255 bytes18.53kB1.4%
util/types.js331 bytes13.89kB2.44%
state/selectors/tooltipSelectors.js84 bytes13.62kB0.62%
state/tooltipSlice.js1.34kB9.16kB17.12%⚠️
chart/RechartsWrapper.js832 bytes8.9kB10.31%⚠️
util/ReactUtils.js2.62kB8.31kB46.14%⚠️
state/mouseEventsMiddleware.js1.44kB4.23kB51.56%⚠️

@codecovCodecov
Copy link

codecovbot commentedJun 25, 2025
edited
Loading

Codecov Report

Attention: Patch coverage is99.59350% with1 line in your changes missing coverage. Please review.

Project coverage is 96.25%. Comparing base(90ccd1b) to head(fd439be).
Report is 17 commits behind head on main.

Files with missing linesPatch %Lines
src/cartesian/Bar.tsx97.36%1 Missing⚠️
Additional details and impacted files
@@            Coverage Diff             @@##             main    #6007      +/-   ##==========================================- Coverage   96.26%   96.25%   -0.02%==========================================  Files         194      198       +4       Lines       19677    19887     +210       Branches     4039     4069      +30     ==========================================+ Hits        18942    19142     +200- Misses        729      739      +10  Partials        6        6

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report?Share it here.

🚀 New features to boost your workflow:
  • ❄️Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@ckiferckifer left a comment

Choose a reason for hiding this comment

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

This PR is going to need some work before we can look at landing it, it looks like there is a lot of unneeded and repeated code as well as tests. Maybe cursor got a little out of hand?

JoaquinPalacios reacted with thumbs up emoji
//@ts-expect-error BarRectangleItem type definition says it's missing properties, but I can see them present in debugger!
constonClick=onClickFromContext(entry,i);
// Always attach external event handlers (passed as props)
consteventHandlers:any={
Copy link
Member

Choose a reason for hiding this comment

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

can this be typed?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

This has been resolved on the commit35036e2

Copy link
Member

Choose a reason for hiding this comment

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

looks like this is still there, just in a different place

externalOnClick?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
externalOnMouseEnter?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
externalOnMouseLeave?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
internalOnClick?:(node:TreemapNode)=>void;
Copy link
Member

Choose a reason for hiding this comment

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

why do we need these? There are already events here?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Hi@ckifer these event handler props are necessary to make sure that the user-provided event handlers are actually invoked when the relevant DOM events occur on the rendered nodes, while still allowing the component to run its own internal logic for those events. I had to come up with this solution otherwise the test "navigates through nested nodes correctly" in line 220 of Treemap.spec.tsx fails because no click handler exist (internalOnClick) and it had no navigation logic (the Treemap doesn't change its view when clicked).

What would you advise me to do in this case? Keep the external event handlers to maintain the component's public API and navigation functionality or remove the navigation test? Perhaps I am missing something else. Let me know, thanks.

Copy link
Member

Choose a reason for hiding this comment

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

trying to understand if these are being exposed in the public interface or not. If so, we can't do this

Comment on lines 222 to 225
/**
* This is a flag that is set when the tooltip is closing.
* This is used to prevent the tooltip from being closed again.
*/
Copy link
Member

Choose a reason for hiding this comment

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

Can't we just check state and derive that the tooltip is not showing?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

The flag prevents race conditions during the brief window when a tooltip is being closed but the middleware hasn't finished processing the close event. I have updated it the comment on a new commitc6d6244 . Please let me know if you consider that I should seek a different solution.

Comment on lines 60 to 65
consthandleRequestTooltipClose=useCallback(()=>{
dispatch(tooltipCloseStart());
setTimeout(()=>{
dispatch(tooltipCloseEnd());
},100);
},[dispatch]);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want to introduce timeouts if we can help it, this seems like something that might cause some funky bugs in the long run

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the feedback, I completely agree about the bugs it could introduce in the long term.

I've updated the PR with a new approach that removes the timer entirely. Now, the mouseMove event immediately following a close action is caught by the middleware, which then "consumes" a gate flag and resets the state. This is now on this new commit5b5258a

Copy link
Contributor

@CopilotCopilotAI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Implements closing click-triggered tooltips when clicking outside the chart area.

  • IntroducesisClosing flag and new Redux actions (tooltipCloseStart,tooltipCloseEnd,clearClickTooltip)
  • Adds outside‐click detection inRechartsWrapper to dispatch tooltip‐close actions
  • Updates chart components (RadialBar, Pie, Scatter, Funnel, Bar) to merge external event handlers with internal tooltip logic and adds comprehensive tests

Reviewed Changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

FileDescription
src/state/tooltipSlice.tsAddisClosing flag, new close actions and reducer handling
src/chart/RechartsWrapper.tsxDetect outside clicks, dispatch close actions with delay, refactor clone
src/chart/{RadialBar, Pie, Scatter…}Merge and forward external event handlers alongside tooltip handlers
test/component/Tooltip/*New and updated tests for outside-click behavior and state expectations
Comments suppressed due to low confidence (3)

src/component/Tooltip.tsx:125

  • The newonRequestTooltipClose prop should be added to public docs/README so users know how to provide a custom close button or logic in custom tooltip content.
  onRequestTooltipClose?: () => void;

test/component/Tooltip/tooltipEventType.spec.tsx:237

  • These debug assertions (expect(trigger).toBeDefined(), etc.) are not needed and clutter the test. Consider removing them once you confirm the variables are properly in scope.
        expect(trigger).toBeDefined();

test/component/Tooltip/tooltipEventType.spec.tsx:359

  • This debug check isn't verifying production behavior and should be removed to keep tests focused on actual tooltip visibility.
        expect(itemSelector).toBeDefined();

Comment on lines 84 to 86
document.addEventListener('mousedown',handleDocumentClick,true);
return()=>{
document.removeEventListener('mousedown',handleDocumentClick,true);
Copy link
Preview

CopilotAIJun 25, 2025

Choose a reason for hiding this comment

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

Consider listening to 'pointerdown' or adding 'touchstart' in addition to 'mousedown' so the outside-​click behavior works on touch devices without delay.

Suggested change
document.addEventListener('mousedown',handleDocumentClick,true);
return()=>{
document.removeEventListener('mousedown',handleDocumentClick,true);
document.addEventListener('mousedown',handleDocumentClick,true);
document.addEventListener('pointerdown',handleDocumentClick,true);
return()=>{
document.removeEventListener('mousedown',handleDocumentClick,true);
document.removeEventListener('pointerdown',handleDocumentClick,true);

Copilot uses AI. Check for mistakes.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Added 'pointerdown' on the latest commit

Simplify event handling in Bar, Funnel, Scatter, Pie, and RadialBar byremoving manual event handler combination. Context hooks already handleboth internal and external handlers.
Remove unnecessary React.cloneElement from RechartsWrapper
Extract repeated event handler pattern across chart components into a reusableutility function to improve maintainability and reduce code duplication.- Add createEventHandlers() utility in ReactUtils.ts- Define proper TypeScript types: TriggerInfo, ContextHandler, ContextHandlers- Refactor Bar, Funnel, Scatter, Pie, and RadialBar components- Fix TriggerInfo type definition for proper TooltipPayload handling- Remove explicit type annotations to avoid conflicts
isClosing type comment updated
fix: replace tooltip timeout with event-driven closingRemove arbitrary 100ms timeout and use event-driven approach for tooltipclosing. This eliminates race conditions and improves reliability byconsuming the isClosing gate in mouseEventsMiddleware instead of guessingtiming with setTimeout.
…pClose propRemove onRequestTooltipClose callback prop and simplify click-triggeredtooltip close handling by directly dispatching clearClickTooltip action.This eliminates the intermediate callback layer and makes the tooltipclose mechanism more direct and maintainable.
@ckifer
Copy link
Member

Will take a look at the changes tomorrow my time, thanks for making them!

JoaquinPalacios reacted with thumbs up emoji

Remove the isClosing state and tooltipCloseStart/tooltipCloseEnd actions from the tooltip system. The tooltip closing functionality now exclusively uses the existing clearClickTooltip action, which is dispatched directly by the middleware for "tap away" scenarios and by the useEffect for "outside clicks" in RechartsWrapper.This simplifies the tooltip state management and removes unnecessary complexity while maintaining the same functionality for closing click-triggered tooltips.- Remove isClosing property from TooltipState type- Remove tooltipCloseStart and tooltipCloseEnd actions- Update test files to remove references to deleted state and actions
clean up Treemap tests
Add 'pointerdown' listener for better outside-click on touch devices
@JoaquinPalacios
Copy link
ContributorAuthor

Hi@ckifer thanks for all your feedback and thorough check of the PR. I ran into some original errors while trying to implement this feature that led to the addition of unnecessary code and a bit of overuse of Cursor. I’ve now addressed most of the comments and cleaned up the repeated code and tests. There is one question for you on one of the comments. Please let me know about it and if there is anything else I should look into it.

Copy link
Member

@ckiferckifer left a comment

Choose a reason for hiding this comment

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

Looking better, thank you. I still want to pull this down and test it a bit. Added a few nits and questions

Comment on lines -684 to -690
if(isLeaf||type==='nest'){
event={
onMouseEnter:this.handleMouseEnter.bind(this,nodeProps),
onMouseLeave:this.handleMouseLeave.bind(this,nodeProps),
onClick:this.handleClick.bind(this,nodeProps),
};
}
Copy link
Member

Choose a reason for hiding this comment

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

doesn't this break nesting functionality?

PavelVanecek reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also why is this change here? How is it related to tooltip closing?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

No on my end, is there anything in particular I should test to dig further?

constcolors=colorPanel||COLOR_PANEL;
return(
<g>
<gonMouseEnter={onMouseEnter}onMouseLeave={onMouseLeave}onClick={onClick}>
Copy link
Member

Choose a reason for hiding this comment

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

why move these to the group?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Screenshot 2025-07-04 at 4 07 19 pm

If not, this test fails.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Please let me know if I should seek a different solution.

externalOnClick?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
externalOnMouseEnter?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
externalOnMouseLeave?:(node:TreemapNode,e:React.MouseEvent<SVGPathElement,MouseEvent>)=>void;
internalOnClick?:(node:TreemapNode)=>void;
Copy link
Member

Choose a reason for hiding this comment

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

trying to understand if these are being exposed in the public interface or not. If so, we can't do this

Comment on lines 764 to 766
onClick:props.onClick,
onMouseEnter:props.onMouseEnter,
onMouseLeave:props.onMouseLeave,
Copy link
Member

Choose a reason for hiding this comment

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

do these need included here? I think they just get filtered out below?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

You're right, those lines are redundant. The event handlers are passed down through the rest of the props and picked up by createEventHandlers. I'd removed them on the commitbbcc682

contextHandlers:ContextHandlers,
):any=>{
// Always attach external event handlers (passed as props)
consteventHandlers:any={
Copy link
Member

Choose a reason for hiding this comment

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

Can this be typed?

PavelVanecek reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also can this be in separate PR?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Hi@ckifer and@PavelVanecek I really tried to typed properly but I couldn't as the Bar.tsx expects one type and the rest another, and both won't accept to have both types on it. Happy to create another PR, question about how should I integrate this with my current PR as well?

),
mouseHoverSelector:funnelChartMouseHoverTooltipSelector,
expectedTransform:'transform: translate(355px, 55px);',
expectedTransform:'transform: translate(40px, 15px);',
Copy link
Member

Choose a reason for hiding this comment

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

why did this change?

PavelVanecek reacted with thumbs up emoji
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like a bug to me. Why did this coordinate change? I do not see how it's related to the PR target.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

I had reverted back he changes and updated the trigerInfo function in the Funnel.tsx and solved the issue. The tooltip was being positioned at the top-left corner of the funnel segment instead of its center. I've corrected the logic to use the proper tooltipPosition, and the test now passes with the original expected value. See this commit
1969150 about it.

@ckiferckifer requested a review fromPavelVanecekJuly 1, 2025 23:41
Copy link
Collaborator

@PavelVanecekPavelVanecek left a comment

Choose a reason for hiding this comment

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

Little bit larger than I would like but okay. Can you please figure out the failing test, and mocks, before merging?

Comment on lines -684 to -690
if(isLeaf||type==='nest'){
event={
onMouseEnter:this.handleMouseEnter.bind(this,nodeProps),
onMouseLeave:this.handleMouseLeave.bind(this,nodeProps),
onClick:this.handleClick.bind(this,nodeProps),
};
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also why is this change here? How is it related to tooltip closing?

activeCoordinate:activeProps.activeCoordinate,
}),
);
}else{
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like the only relevant change to the PR title.

contextHandlers:ContextHandlers,
):any=>{
// Always attach external event handlers (passed as props)
consteventHandlers:any={
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also can this be in separate PR?

),
mouseHoverSelector:funnelChartMouseHoverTooltipSelector,
expectedTransform:'transform: translate(355px, 55px);',
expectedTransform:'transform: translate(40px, 15px);',
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like a bug to me. Why did this coordinate change? I do not see how it's related to the PR target.

store=createMinimalStore();

// Mock the selector to return valid active props
constmockSelectActivePropsFromChartPointer=vi.mocked(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why so many mocks? Can we replace the mocks with actual interactions? That way we will not depend on the selector/state implementation.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

I have simplified and updated the test file with actual interactions on this commitfd439be

Move noop function definition to createEventHandlers utility and remove redundant noop imports from individual components.
Commen removed as per request
return null back as it was
@JoaquinPalacios
Copy link
ContributorAuthor

Hi@ckifer this is in response tothis comment that I cannot reply below it.

As per my understanding of the code the externalOnClick, externalOnMouseEnter, externalOnMouseLeave, and internalOnClick props are not part of the public API. They are internal props within the ContentItemWithEvents component.

Remove redundant event handler props
The tooltip for the FunnelChart was being positioned at the top-left corner of the trapezoid segment instead of its center.This commit updates the event handler logic to use the correctly calculated `entry.tooltipPosition`, which ensures the tooltip appears centered on the active funnel segment. This resolves the failing Tooltip visibility test and aligns the component's behavior with the expected output.
The tests cover two key scenarios:- Clearing an active click-tooltip when the user clicks on an empty chart area.- Ensuring the tooltip state remains unchanged if no tooltip is active during the click.This validates the "tap-away" to dismiss functionality.
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@ckiferckiferckifer requested changes

Copilot code reviewCopilotCopilot left review comments

@PavelVanecekPavelVanecekPavelVanecek requested changes

Requested changes must be addressed to merge this pull request.

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

3 participants
@JoaquinPalacios@ckifer@PavelVanecek

[8]ページ先頭

©2009-2025 Movatter.jp