From ee89858c43cf47f9a160de435dce65a6d691947e Mon Sep 17 00:00:00 2001 From: Agustin Delgado Date: Tue, 20 Oct 2026 14:46:18 -0300 Subject: [PATCH] fix(tooltip): a tooltip that follows the pointer stays in the box that clips it --- .changeset/tooltip-clip-bounds.md | 5 +++++ docs/src/content/tooltip/index.md | 2 +- .../src/lib/tooltip/chart-tooltip.svelte | 21 ++++++++++++++----- .../src/lib/tooltip/chart-tooltip.test.ts | 9 ++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .changeset/tooltip-clip-bounds.md diff --git a/.changeset/tooltip-clip-bounds.md b/.changeset/tooltip-clip-bounds.md new file mode 100644 index 0000000..dd23423 --- /dev/null +++ b/.changeset/tooltip-clip-bounds.md @@ -0,0 +1,5 @@ +--- +'@human-kit/charts': patch +--- + +A `Chart.Tooltip` that follows the pointer stays in the nearest ancestor that clips it, not in the page: a page frame with `overflow: hidden` no longer cuts it. diff --git a/docs/src/content/tooltip/index.md b/docs/src/content/tooltip/index.md index d1dc153..89d52eb 100644 --- a/docs/src/content/tooltip/index.md +++ b/docs/src/content/tooltip/index.md @@ -41,7 +41,7 @@ The tooltip is HTML, thus it goes in `Chart.Root` and not in `Chart.Plot`. It ma `offset` is the distance between the point and the tooltip, in pixels. The tooltip is above the point, and its middle is at the x position of the point. -With `follow`, the tooltip goes to the pointer and moves with it, over the point under the pointer. Use it for a large mark, as a long bar, whose position can be far from the pointer. The point that has the keyboard focus keeps its tooltip at the point. A tooltip that follows the pointer stays in the width of the page instead of the figure: on a figure narrower than the tooltip, it still goes where the pointer goes. +With `follow`, the tooltip goes to the pointer and moves with it, over the point under the pointer. Use it for a large mark, as a long bar, whose position can be far from the pointer. The point that has the keyboard focus keeps its tooltip at the point. A tooltip that follows the pointer stays in the box that would cut it (the nearest ancestor whose `overflow` is not `visible`, or the page) instead of the figure: on a figure narrower than the tooltip, it still goes where the pointer goes. ## Content of your own diff --git a/packages/charts/src/lib/tooltip/chart-tooltip.svelte b/packages/charts/src/lib/tooltip/chart-tooltip.svelte index b94f7ec..cbebee6 100644 --- a/packages/charts/src/lib/tooltip/chart-tooltip.svelte +++ b/packages/charts/src/lib/tooltip/chart-tooltip.svelte @@ -94,8 +94,8 @@ // The figure is the containing block of the tooltip, thus the position adds the distance from // the padding edge of the figure to the plot. The tooltip shows only in a browser. // `bounds` is the space the tooltip stays in, from the padding edge of the figure: the width of - // the figure, or the width of the page for a tooltip that follows the pointer, which must go - // wherever the pointer goes on a figure narrower than itself. + // the figure, or for a tooltip that follows the pointer —which must go wherever the pointer goes + // on a figure narrower than itself— the width of the box that would cut it. const placement = $derived.by(() => { const plot = ctx.plotElement; const figure = plot?.closest('figure'); @@ -105,12 +105,23 @@ const start = b.left + figure.clientLeft; return { offset: [a.left - start, a.top - b.top - figure.clientTop], - bounds: follow - ? [-start, document.documentElement.clientWidth - start] - : [0, figure.clientWidth] + bounds: follow ? clipBounds(figure).map((x) => x - start) : [0, figure.clientWidth] }; }); + /** + * The left and the right of the nearest ancestor that cuts what overflows it, on the screen, + * or of the page without one. + */ + function clipBounds(figure: Element): [number, number] { + for (let node = figure.parentElement; node; node = node.parentElement) { + if (getComputedStyle(node).overflowX === 'visible') continue; + const box = node.getBoundingClientRect(); + return [box.left + node.clientLeft, box.left + node.clientLeft + node.clientWidth]; + } + return [0, document.documentElement.clientWidth]; + } + // Near an edge of its bounds, the tooltip moves away from the point. Thus it does not make the // page wider on a narrow screen. let width = $state(0); diff --git a/packages/charts/src/lib/tooltip/chart-tooltip.test.ts b/packages/charts/src/lib/tooltip/chart-tooltip.test.ts index d574886..aa23c7d 100644 --- a/packages/charts/src/lib/tooltip/chart-tooltip.test.ts +++ b/packages/charts/src/lib/tooltip/chart-tooltip.test.ts @@ -50,6 +50,15 @@ describe('Chart.Tooltip', () => { expect(box.right).toBeLessThanOrEqual(document.documentElement.clientWidth + 0.5); }); + it('follows the pointer only as far as the box that cuts it', async () => { + const { container } = render(PartsTest, { follow: true, wide: true }); + container.style.cssText = 'overflow: hidden; width: 320px; padding: 0 10px'; + const clip = container.getBoundingClientRect(); + const point = points()[5]; + await hoverAt(Number(point.getAttribute('cx')), Number(point.getAttribute('cy'))); + expect(tooltip()!.getBoundingClientRect().right).toBeLessThanOrEqual(clip.right + 0.5); + }); + it('keeps the tooltip of the focused point at the point when it follows the pointer', async () => { render(PartsTest, { follow: true }); // The pointer was in the plot and left it: the focus has no pointer to follow.