|
77 | 77 | break; |
78 | 78 | } |
79 | 79 |
|
80 | | - // Clamp horizontal position to keep tooltip within viewport |
81 | | - if (finalPosition === 'bottom' || finalPosition === 'top') { |
82 | | - const halfWidth = tooltipMaxWidth / 2; |
83 | | - if (x - halfWidth < padding) { |
84 | | - x = padding + halfWidth; |
85 | | - } else if (x + halfWidth > window.innerWidth - padding) { |
86 | | - x = window.innerWidth - padding - halfWidth; |
87 | | - } |
88 | | - } |
89 | | -
|
90 | | - // Clamp vertical position for left/right tooltips |
91 | | - if (finalPosition === 'left' || finalPosition === 'right') { |
92 | | - const halfHeight = tooltipHeight / 2; |
93 | | - if (y - halfHeight < padding) { |
94 | | - y = padding + halfHeight; |
95 | | - } else if (y + halfHeight > window.innerHeight - padding) { |
96 | | - y = window.innerHeight - padding - halfHeight; |
97 | | - } |
98 | | - } |
99 | | -
|
| 80 | + // Viewport clamping happens in the component after render, |
| 81 | + // where the actual tooltip dimensions can be measured. |
100 | 82 | tooltipStore.set({ text, shortcut, maxWidth, x, y, visible: true, position: finalPosition }); |
101 | 83 | }, 50); |
102 | 84 | } |
|
152 | 134 | </script> |
153 | 135 |
|
154 | 136 | <script lang="ts"> |
155 | | - let state = $state<TooltipState>({ text: '', x: 0, y: 0, visible: false, position: 'bottom' }); |
| 137 | + let tip = $state<TooltipState>({ text: '', x: 0, y: 0, visible: false, position: 'bottom' }); |
| 138 | + let tooltipEl = $state<HTMLDivElement>(); |
156 | 139 |
|
157 | 140 | tooltipStore.subscribe((s) => { |
158 | | - state = s; |
| 141 | + tip = s; |
| 142 | + }); |
| 143 | +
|
| 144 | + // Clamp the rendered tooltip to the viewport using its measured size, |
| 145 | + // so narrow tooltips sit flush at the edges instead of being pushed |
| 146 | + // inward by a max-width estimate. |
| 147 | + $effect(() => { |
| 148 | + if (!tip.visible || !tooltipEl) return; |
| 149 | + void tip.text; void tip.shortcut; void tip.position; |
| 150 | + const padding = 8; // Minimum distance from viewport edge |
| 151 | + // Measure from the unclamped anchor position — a previous run may |
| 152 | + // have shifted the element while Svelte skipped the style attribute. |
| 153 | + tooltipEl.style.left = `${tip.x}px`; |
| 154 | + tooltipEl.style.top = `${tip.y}px`; |
| 155 | + const rect = tooltipEl.getBoundingClientRect(); |
| 156 | + let dx = 0; |
| 157 | + let dy = 0; |
| 158 | + if (rect.left < padding) { |
| 159 | + dx = padding - rect.left; |
| 160 | + } else if (rect.right > window.innerWidth - padding) { |
| 161 | + dx = window.innerWidth - padding - rect.right; |
| 162 | + } |
| 163 | + if (rect.top < padding) { |
| 164 | + dy = padding - rect.top; |
| 165 | + } else if (rect.bottom > window.innerHeight - padding) { |
| 166 | + dy = window.innerHeight - padding - rect.bottom; |
| 167 | + } |
| 168 | + if (dx !== 0) tooltipEl.style.left = `${tip.x + dx}px`; |
| 169 | + if (dy !== 0) tooltipEl.style.top = `${tip.y + dy}px`; |
159 | 170 | }); |
160 | 171 | </script> |
161 | 172 |
|
162 | | -{#if state.visible} |
| 173 | +{#if tip.visible} |
163 | 174 | <div |
164 | | - class="tooltip tooltip-{state.position}" |
165 | | - style="left: {state.x}px; top: {state.y}px;{state.maxWidth ? ` max-width: ${state.maxWidth}px;` : ''}" |
| 175 | + bind:this={tooltipEl} |
| 176 | + class="tooltip tooltip-{tip.position}" |
| 177 | + style="left: {tip.x}px; top: {tip.y}px;{tip.maxWidth ? ` max-width: ${tip.maxWidth}px;` : ''}" |
166 | 178 | > |
167 | | - <span class="text">{state.text}</span> |
168 | | - {#if state.shortcut} |
169 | | - <span class="shortcut">{state.shortcut}</span> |
| 179 | + <span class="text">{tip.text}</span> |
| 180 | + {#if tip.shortcut} |
| 181 | + <span class="shortcut">{tip.shortcut}</span> |
170 | 182 | {/if} |
171 | 183 | </div> |
172 | 184 | {/if} |
|
0 commit comments