diff --git a/.changeset/tooltip-follow-axis.md b/.changeset/tooltip-follow-axis.md new file mode 100644 index 0000000..f34debb --- /dev/null +++ b/.changeset/tooltip-follow-axis.md @@ -0,0 +1,5 @@ +--- +'@human-kit/charts': patch +--- + +A `Chart.Tooltip` that follows the pointer moves only along it, at the height of the point. The tooltip is centred with `translate` instead of `transform`, thus a CSS animation of `transform` no longer moves it to the side while it opens or closes. diff --git a/docs/src/content/tooltip/api.json b/docs/src/content/tooltip/api.json index 7f1b420..f9ae10a 100644 --- a/docs/src/content/tooltip/api.json +++ b/docs/src/content/tooltip/api.json @@ -25,7 +25,7 @@ "type": "boolean", "required": false, "default": "false", - "description": "Places the tooltip at the pointer and moves it with the pointer, over the point under it.\nFor a large mark, as a long bar, whose anchor can be far from the pointer. The point that\nhas the keyboard focus keeps its tooltip at the point. The default is `false`." + "description": "Moves the tooltip along with the pointer, at the height of the point under it. For a large\nmark, as a long bar, whose anchor can be far from the pointer. The point that has the\nkeyboard focus keeps its tooltip at the point. The default is `false`." }, { "name": "class", diff --git a/docs/src/content/tooltip/index.md b/docs/src/content/tooltip/index.md index 89d52eb..87f19a7 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 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. +With `follow`, the tooltip moves along with the pointer, at the height of 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 @@ -81,7 +81,7 @@ The last three attributes are on the default content only. ### Animation -The box has `data-entering` while it opens and `data-exiting` while it closes. Give them a CSS animation: the box goes after the animation of `data-exiting` ends. Without an animation, it goes at once. +The box has `data-entering` while it opens and `data-exiting` while it closes. It is centred with the `translate` property, thus an animation of `transform` does not move it away from its place. Give them a CSS animation: the box goes after the animation of `data-exiting` ends. Without an animation, it goes at once. ```css @keyframes tooltip-in { diff --git a/packages/charts/src/lib/tooltip/chart-tooltip.svelte b/packages/charts/src/lib/tooltip/chart-tooltip.svelte index cbebee6..eda7280 100644 --- a/packages/charts/src/lib/tooltip/chart-tooltip.svelte +++ b/packages/charts/src/lib/tooltip/chart-tooltip.svelte @@ -29,9 +29,10 @@ const point = entry.points[index]; const anchor = ctx.anchor(entry.mark, entry.series, index); if (!anchor) return null; - // A tooltip that follows the pointer goes with it over the point under it. The point that has - // the keyboard focus has no pointer, thus its tooltip stays at the point. - const [x, y] = follow && pointer && id === hoveredId ? pointer : anchor; + // A tooltip that follows the pointer goes along with it, at the height of the point under it. + // The point that has the keyboard focus has no pointer, thus its tooltip stays at the point. + const x = follow && pointer && id === hoveredId ? pointer[0] : anchor[0]; + const y = anchor[1]; return { point, x, @@ -179,7 +180,7 @@ bind:offsetWidth={width} style:left="{left}px" style:top="{placement.offset[1] + tip.y - offset}px" - style:transform="translate(-50%, -100%)" + style:translate="-50% -100%" style:pointer-events="none" {...restProps} > diff --git a/packages/charts/src/lib/tooltip/chart-tooltip.test.ts b/packages/charts/src/lib/tooltip/chart-tooltip.test.ts index aa23c7d..19609b1 100644 --- a/packages/charts/src/lib/tooltip/chart-tooltip.test.ts +++ b/packages/charts/src/lib/tooltip/chart-tooltip.test.ts @@ -33,7 +33,8 @@ describe('Chart.Tooltip', () => { const above = document.querySelector('svg')!.getBoundingClientRect().top - document.querySelector('figure')!.getBoundingClientRect().top; - expect(position()[1]).toBeCloseTo(above + cy + 10 - 8, 0); + // Only along: at the height of the point, not of the pointer. + expect(position()[1]).toBeCloseTo(above + cy - 8, 0); await hoverAt(cx + 24, cy + 10); expect(position()[0]).toBeCloseTo(cx + 24, 0); @@ -71,6 +72,27 @@ describe('Chart.Tooltip', () => { expect(position()[0]).toBeCloseTo(Number(point.getAttribute('cx')), 0); }); + it('stays in its place while an animation of transform closes it', async () => { + const style = document.createElement('style'); + style.textContent = ` + @keyframes out { to { transform: scale(0.9) } } + [data-tooltip][data-exiting] { animation: out 400ms } + `; + document.head.append(style); + try { + render(PartsTest); + points()[0].focus(); + await userEvent.keyboard('{ArrowRight}'); + const open = tooltip()!.getBoundingClientRect(); + await userEvent.keyboard('{Escape}'); + const closing = tooltip()!.getBoundingClientRect(); + expect(closing.left + closing.width / 2).toBeCloseTo(open.left + open.width / 2, 0); + expect(closing.bottom).toBeLessThanOrEqual(open.bottom + 0.5); + } finally { + style.remove(); + } + }); + it('marks the opening for CSS, and closes after the animation of the closing', async () => { const style = document.createElement('style'); style.textContent = ` diff --git a/packages/charts/src/lib/types.ts b/packages/charts/src/lib/types.ts index 5cec0ab..9a7d87b 100644 --- a/packages/charts/src/lib/types.ts +++ b/packages/charts/src/lib/types.ts @@ -296,9 +296,9 @@ export type ChartTooltipProps = Omit, 'class' | ' /** The distance between the point and the tooltip, in pixels. The default is 8. */ offset?: number; /** - * Places the tooltip at the pointer and moves it with the pointer, over the point under it. - * For a large mark, as a long bar, whose anchor can be far from the pointer. The point that - * has the keyboard focus keeps its tooltip at the point. The default is `false`. + * Moves the tooltip along with the pointer, at the height of the point under it. For a large + * mark, as a long bar, whose anchor can be far from the pointer. The point that has the + * keyboard focus keeps its tooltip at the point. The default is `false`. */ follow?: boolean; /**