diff --git a/src/components/Chart/BarChart.tsx b/src/components/Chart/BarChart.tsx index 42f24d8..3d9aa9b 100644 --- a/src/components/Chart/BarChart.tsx +++ b/src/components/Chart/BarChart.tsx @@ -259,9 +259,18 @@ export const Bar = React.forwardRef( if (tip) { if (isHorizontal) { const absY = PAD_TOP + (idx + 0.5) * slotSize; + const rightAnchor = padLeft + plotWidth + TOOLTIP_GAP; tip.style.top = `${absY}px`; - tip.style.left = `${padLeft + plotWidth + 8}px`; - tip.style.transform = 'none'; + tip.style.display = ''; + const tipW = tip.offsetWidth; + const fitsRight = rightAnchor + tipW <= width; + if (fitsRight) { + tip.style.left = `${rightAnchor}px`; + tip.style.transform = 'none'; + } else { + tip.style.left = `${padLeft + plotWidth - TOOLTIP_GAP}px`; + tip.style.transform = 'translateX(-100%)'; + } } else { const absX = padLeft + (idx + 0.5) * slotSize; const totalW = padLeft + plotWidth + padRight; @@ -281,7 +290,7 @@ export const Bar = React.forwardRef( tip.style.display = ''; } }, - [data.length, categoryLength, padLeft, padRight, slotSize, isHorizontal, plotWidth], + [data.length, categoryLength, padLeft, padRight, slotSize, isHorizontal, plotWidth, width], ); const handleMouseLeave = React.useCallback(() => { @@ -365,9 +374,18 @@ export const Bar = React.forwardRef( const tip = tooltipRef.current; if (tip) { if (isHorizontal) { + const rightAnchor = padLeft + plotWidth + TOOLTIP_GAP; tip.style.top = `${PAD_TOP + (next + 0.5) * slotSize}px`; - tip.style.left = `${padLeft + plotWidth + 8}px`; - tip.style.transform = 'none'; + tip.style.display = ''; + const tipW = tip.offsetWidth; + const fitsRight = rightAnchor + tipW <= width; + if (fitsRight) { + tip.style.left = `${rightAnchor}px`; + tip.style.transform = 'none'; + } else { + tip.style.left = `${padLeft + plotWidth - TOOLTIP_GAP}px`; + tip.style.transform = 'translateX(-100%)'; + } } else { const absX = padLeft + (next + 0.5) * slotSize; const totalW = padLeft + plotWidth + padRight; @@ -387,7 +405,7 @@ export const Bar = React.forwardRef( tip.style.display = ''; } }, - [activeIndex, data, slotSize, padLeft, padRight, plotWidth, isHorizontal, onClickDatum, trackedClick, handleMouseLeave], + [activeIndex, data, slotSize, padLeft, padRight, plotWidth, isHorizontal, onClickDatum, trackedClick, handleMouseLeave, width], ); const interactive = interactiveProp; diff --git a/src/components/Chart/Chart.test-stories.tsx b/src/components/Chart/Chart.test-stories.tsx index 7f2639e..d3b4fb2 100644 --- a/src/components/Chart/Chart.test-stories.tsx +++ b/src/components/Chart/Chart.test-stories.tsx @@ -383,6 +383,26 @@ export function BarBasic() { ); } +export function BarHorizontalNarrow() { + return ( +
+ +
+ ); +} + export function SankeyBasic() { return ( { await expect(tooltip).toBeHidden(); }); + test('Horizontal bar chart: tooltip stays within the container on hover, not cropped off the right edge', async ({ mount, page }) => { + await mount(); + const container = page.locator('[data-testid="bar-chart-horizontal"]'); + const svg = container.locator('svg'); + const svgBox = await svg.boundingBox(); + if (!svgBox) throw new Error('svg not measured'); + await page.mouse.move(svgBox.x + svgBox.width / 2, svgBox.y + svgBox.height - 10); + const tooltip = container.locator('> div[class*="tooltip"]').first(); + await expect(tooltip).toBeVisible(); + const containerBox = await container.boundingBox(); + const tooltipBox = await tooltip.boundingBox(); + if (!containerBox || !tooltipBox) throw new Error('boxes not measured'); + expect(tooltipBox.x + tooltipBox.width).toBeLessThanOrEqual(containerBox.x + containerBox.width + 1); + }); + test('Scatter chart: arrow keys show tooltip, Escape dismisses', async ({ mount, page }) => { await mount(); const svg = page.locator('[data-testid="scatter-chart"] svg');