Skip to content

Commit 7840a0e

Browse files
committed
Add dynamic tooltip positioning to avoid viewport clipping
1 parent 3d3cb65 commit 7840a0e

1 file changed

Lines changed: 56 additions & 19 deletions

File tree

src/lib/components/common/Tooltip.svelte

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,70 @@
110110
</script>
111111

112112
<script lang="ts">
113+
import { tick } from 'svelte';
114+
113115
let state = $state<TooltipState>({ text: '', x: 0, y: 0, visible: false, position: 'bottom' });
116+
let tooltipEl: HTMLDivElement | undefined = $state();
117+
let adjustment = $state({ x: 0, y: 0 });
114118
115-
tooltipStore.subscribe((s) => {
119+
tooltipStore.subscribe(async (s) => {
116120
state = s;
121+
adjustment = { x: 0, y: 0 }; // Reset adjustment
122+
if (s.visible) {
123+
// Wait for DOM update, then check viewport collision
124+
await tick();
125+
adjustPosition();
126+
}
127+
});
128+
129+
function adjustPosition() {
130+
if (!tooltipEl) return;
131+
132+
const rect = tooltipEl.getBoundingClientRect();
133+
const padding = 8;
134+
let adjustX = 0;
135+
let adjustY = 0;
136+
137+
// Check horizontal overflow
138+
if (rect.left < padding) {
139+
adjustX = padding - rect.left;
140+
} else if (rect.right > window.innerWidth - padding) {
141+
adjustX = window.innerWidth - padding - rect.right;
142+
}
143+
144+
// Check vertical overflow
145+
if (rect.top < padding) {
146+
adjustY = padding - rect.top;
147+
} else if (rect.bottom > window.innerHeight - padding) {
148+
adjustY = window.innerHeight - padding - rect.bottom;
149+
}
150+
151+
adjustment = { x: adjustX, y: adjustY };
152+
}
153+
154+
// Compute full transform based on position and adjustment
155+
let transform = $derived.by(() => {
156+
const { x, y } = adjustment;
157+
const adj = (x !== 0 || y !== 0) ? ` translate(${x}px, ${y}px)` : '';
158+
switch (state.position) {
159+
case 'top':
160+
return `translateX(-50%) translateY(-100%)${adj}`;
161+
case 'left':
162+
return `translateX(-100%) translateY(-50%)${adj}`;
163+
case 'right':
164+
return `translateY(-50%)${adj}`;
165+
case 'bottom':
166+
default:
167+
return `translateX(-50%)${adj}`;
168+
}
117169
});
118170
</script>
119171

120172
{#if state.visible}
121173
<div
122-
class="tooltip tooltip-{state.position}"
123-
style="left: {state.x}px; top: {state.y}px;{state.maxWidth ? ` max-width: ${state.maxWidth}px;` : ''}"
174+
bind:this={tooltipEl}
175+
class="tooltip"
176+
style="left: {state.x}px; top: {state.y}px; transform: {transform};{state.maxWidth ? ` max-width: ${state.maxWidth}px;` : ''}"
124177
>
125178
<span class="text">{state.text}</span>
126179
{#if state.shortcut}
@@ -155,22 +208,6 @@
155208
white-space: nowrap;
156209
}
157210
158-
.tooltip-bottom {
159-
transform: translateX(-50%);
160-
}
161-
162-
.tooltip-top {
163-
transform: translateX(-50%) translateY(-100%);
164-
}
165-
166-
.tooltip-left {
167-
transform: translateX(-100%) translateY(-50%);
168-
}
169-
170-
.tooltip-right {
171-
transform: translateY(-50%);
172-
}
173-
174211
@keyframes fadeIn {
175212
from { opacity: 0; }
176213
to { opacity: 1; }

0 commit comments

Comments
 (0)