Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-tooltips-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

fix: prevent inactive tooltips from causing document overflow
6 changes: 3 additions & 3 deletions packages/mermaid/src/diagrams/agentflow/agentflowDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import { createTooltip, hideTooltip } from '../common/svgDrawCommon.js';
import type {
AgentflowElementMapping,
AgentflowSemanticModel,
Expand Down Expand Up @@ -1113,7 +1113,7 @@ You have to call mermaid.initialize.`
}
const rect = (e.currentTarget as Element)?.getBoundingClientRect();

tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem.interrupt().transition().duration(200).style('opacity', '.9');
tooltipElem
.text(el.attr('title'))
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
Expand All @@ -1122,7 +1122,7 @@ You have to call mermaid.initialize.`
el.classed('hover', true);
})
.on('mouseout', (e: MouseEvent) => {
tooltipElem.transition().duration(500).style('opacity', 0);
hideTooltip(tooltipElem);
const el = select(e.currentTarget as Element);
el.classed('hover', false);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/mermaid/src/diagrams/class/classDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import { createTooltip, hideTooltip } from '../common/svgDrawCommon.js';
import { ClassMember } from './classTypes.js';
import type {
ClassRelation,
Expand Down Expand Up @@ -538,7 +538,7 @@ export class ClassDB implements DiagramDB {
}

const rect = (event.currentTarget as Element).getBoundingClientRect();
tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem.interrupt().transition().duration(200).style('opacity', '.9');
tooltipElem
.html(DOMPurify.sanitize(title))
.style('left', `${window.scrollX + rect.left + rect.width / 2}px`)
Expand All @@ -547,7 +547,7 @@ export class ClassDB implements DiagramDB {
el.classed('hover', true);
})
.on('mouseout', (event: MouseEvent) => {
tooltipElem.transition().duration(500).style('opacity', 0);
hideTooltip(tooltipElem);
select(event.currentTarget as HTMLElement).classed('hover', false);
});
};
Expand Down
10 changes: 10 additions & 0 deletions packages/mermaid/src/diagrams/common/svgDrawCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ export const createTooltip = () => {
.attr('class', 'mermaidTooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('top', '0px')
.style('left', '0px')
.style('text-align', 'center')
.style('max-width', '200px')
.style('padding', '2px')
Expand All @@ -157,3 +159,11 @@ export const createTooltip = () => {
}
return tooltipElem;
};

export const hideTooltip = (tooltipElem: ReturnType<typeof createTooltip>) => {
return tooltipElem
.transition()
.duration(500)
.style('opacity', 0)
.on('end', () => tooltipElem.html('').style('top', '0px').style('left', '0px'));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
89 changes: 89 additions & 0 deletions packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,95 @@
import { FlowDB } from './flowDb.js';
import type { FlowSubGraph, FlowText } from './types.js';
import flow from './parser/flowParser.js';
import { jsdomIt } from '../../tests/util.js';

describe('flow db tooltips', () => {
const bindTooltip = () => {
document.body.innerHTML = `
<div id="diagram">
<svg>
<g class="node" title="Node details"></g>
<g class="node" title="Second node details"></g>
</svg>
</div>
`;
const diagram = document.querySelector('#diagram');
if (!diagram) {
throw new Error('Expected diagram fixture');
}

const flowDb = new FlowDB();
flowDb.bindFunctions(diagram);

const [node, secondNode] = document.querySelectorAll<SVGGElement>('g.node');
const tooltip = document.querySelector<HTMLDivElement>('.mermaidTooltip');
if (!node || !secondNode || !tooltip) {
throw new Error('Expected tooltip fixture');
}
return { node, secondNode, tooltip };
};

jsdomIt('positions an inactive tooltip at the document origin', () => {
const { tooltip } = bindTooltip();

expect(tooltip.style.top).toBe('0px');
expect(tooltip.style.left).toBe('0px');
});

jsdomIt('returns the tooltip to the document origin after it fades out', async () => {
const { node, tooltip } = bindTooltip();
node.getBoundingClientRect = () =>
({
x: 40,
y: 60,
top: 60,
right: 140,
bottom: 100,
left: 40,
width: 100,
height: 40,
toJSON: () => ({}),
}) satisfies DOMRect;

node.dispatchEvent(new window.MouseEvent('mouseover', { bubbles: true }));
expect(tooltip.style.top).toBe('100px');
expect(tooltip.style.left).toBe('90px');

node.dispatchEvent(new window.MouseEvent('mouseout', { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 550));

expect(tooltip.style.top).toBe('0px');
expect(tooltip.style.left).toBe('0px');
expect(tooltip.textContent).toBe('');
});

jsdomIt('keeps a newly hovered tooltip visible when the previous fade is ending', async () => {
const { node, secondNode, tooltip } = bindTooltip();
node.getBoundingClientRect = () =>
({
left: 40,
right: 140,
bottom: 100,
}) as DOMRect;
secondNode.getBoundingClientRect = () =>
({
left: 200,
right: 300,
bottom: 240,
}) as DOMRect;

node.dispatchEvent(new window.MouseEvent('mouseover', { bubbles: true }));
node.dispatchEvent(new window.MouseEvent('mouseout', { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 505));

secondNode.dispatchEvent(new window.MouseEvent('mouseover', { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 40));

expect(tooltip.textContent).toBe('Second node details');
expect(tooltip.style.top).toBe('240px');
expect(tooltip.style.left).toBe('250px');
});
});

describe('flow db subgraphs', () => {
let flowDb: FlowDB;
Expand Down
6 changes: 3 additions & 3 deletions packages/mermaid/src/diagrams/flowchart/flowDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import { createTooltip, hideTooltip } from '../common/svgDrawCommon.js';
import type {
FlowClass,
FlowEdge,
Expand Down Expand Up @@ -628,7 +628,7 @@ You have to call mermaid.initialize.`
}
const rect = (e.currentTarget as Element)?.getBoundingClientRect();

tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem.interrupt().transition().duration(200).style('opacity', '.9');
tooltipElem
.text(el.attr('title'))
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
Expand All @@ -637,7 +637,7 @@ You have to call mermaid.initialize.`
el.classed('hover', true);
})
.on('mouseout', (e: MouseEvent) => {
tooltipElem.transition().duration(500).style('opacity', 0);
hideTooltip(tooltipElem);
const el = select(e.currentTarget as Element);
el.classed('hover', false);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/mermaid/src/diagrams/state/stateDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import { createTooltip } from '../common/svgDrawCommon.js';
import { createTooltip, hideTooltip } from '../common/svgDrawCommon.js';
import { dataFetcher, reset as resetDataFetcher } from './dataFetcher.js';
import { getDir } from './stateRenderer-v3-unified.js';
import {
Expand Down Expand Up @@ -671,15 +671,15 @@ export class StateDB {
}
const rect = (e.currentTarget as Element)?.getBoundingClientRect();

tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem.interrupt().transition().duration(200).style('opacity', '.9');
tooltipElem
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
.style('top', window.scrollY + rect.bottom + 'px');
tooltipElem.html(DOMPurify.sanitize(title));
el.classed('hover', true);
})
.on('mouseout', (e: MouseEvent) => {
tooltipElem.transition().duration(500).style('opacity', 0);
hideTooltip(tooltipElem);
const el = select(e.currentTarget as Element);
el.classed('hover', false);
});
Expand Down
Loading