fix(04): tooltip and dropdown overflow clipping

Tooltip uses fixed positioning on document.body to escape overflow
contexts. Removed overflow-x:auto from container and SVG area that
clipped tooltip top and dropdown focus ring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Mühl 2026-04-08 15:47:39 +02:00
parent dac51a6702
commit c90ad494b7
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
5 changed files with 15 additions and 15 deletions

View file

@ -44,7 +44,6 @@
.heatmap-wrapper .heatmap-svg-area {
flex: 1;
min-width: 0;
overflow-x: auto;
}
.heatmap-wrapper .heatmap-filter {

View file

@ -2188,9 +2188,10 @@ var KimaiHeatmap = (() => {
});
svg.selectAll(".month-label").data(months).join("text").attr("class", "heatmap-label month-label").attr("x", (d) => marginLeft + d.week * step).attr("y", marginTop - 6).text((d) => MONTH_FORMAT(d.date));
svg.selectAll(".day-label").data(DAY_LABELS).join("text").attr("class", "heatmap-label day-label").attr("x", marginLeft - 6).attr("y", (_d, i) => marginTop + i * step + cellSize - 2).attr("text-anchor", "end").text((d) => d);
document.querySelectorAll(".heatmap-tooltip").forEach((el) => el.remove());
const tooltip = createTooltip();
wrapper.style.position = "relative";
wrapper.appendChild(tooltip);
tooltip.style.position = "fixed";
document.body.appendChild(tooltip);
svg.selectAll(".heatmap-cell").data(cells).join("rect").attr(
"class",
(d) => d.entry ? "heatmap-cell" : "heatmap-cell heatmap-empty"
@ -2200,9 +2201,8 @@ var KimaiHeatmap = (() => {
tooltip.innerHTML = `<strong>${DISPLAY_FORMAT(d.date)}</strong><br>${hours}h (${count} entries)`;
tooltip.style.display = "block";
const rect = event.target.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
tooltip.style.left = `${rect.left - wrapperRect.left + cellSize / 2}px`;
tooltip.style.top = `${rect.top - wrapperRect.top - 40}px`;
tooltip.style.left = `${rect.left + cellSize / 2}px`;
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 8}px`;
}).on("mouseleave", function() {
tooltip.style.display = "none";
}).on("click", function(_event, d) {

View file

@ -8,7 +8,7 @@
data-url="{{ path('heatmap_data') }}"
data-projects="{{ data.projects|json_encode }}"
data-timesheet-url="{{ path('timesheet') }}"
style="min-height: 150px; overflow-x: auto;">
style="min-height: 150px;">
</div>
<script src="{{ asset('bundles/kimaiheatmap/heatmap.js') }}"></script>
<script type="text/javascript">

View file

@ -163,10 +163,12 @@ export function renderHeatmap(
.attr('text-anchor', 'end')
.text((d) => d);
// Tooltip
// Tooltip (fixed positioning to escape overflow clipping)
// Remove any stale tooltip from previous renders
document.querySelectorAll('.heatmap-tooltip').forEach(el => el.remove());
const tooltip = createTooltip();
wrapper.style.position = 'relative';
wrapper.appendChild(tooltip);
tooltip.style.position = 'fixed';
document.body.appendChild(tooltip);
// Cells
svg
@ -188,9 +190,8 @@ export function renderHeatmap(
tooltip.style.display = 'block';
const rect = (event.target as SVGRectElement).getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
tooltip.style.left = `${rect.left - wrapperRect.left + cellSize / 2}px`;
tooltip.style.top = `${rect.top - wrapperRect.top - 40}px`;
tooltip.style.left = `${rect.left + cellSize / 2}px`;
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 8}px`;
})
.on('mouseleave', function () {
tooltip.style.display = 'none';

View file

@ -94,7 +94,7 @@ describe('renderHeatmap', () => {
// jsdom getBoundingClientRect returns zeros, which is fine for structure test
rect!.dispatchEvent(event);
const tooltip = container.querySelector('.heatmap-tooltip') as HTMLDivElement;
const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement;
expect(tooltip).not.toBeNull();
expect(tooltip.style.display).toBe('block');
expect(tooltip.innerHTML).toContain('h');
@ -110,7 +110,7 @@ describe('renderHeatmap', () => {
rect!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
rect!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
const tooltip = container.querySelector('.heatmap-tooltip') as HTMLDivElement;
const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement;
expect(tooltip.style.display).toBe('none');
});