|
1 | 1 | <script lang="ts"> |
| 2 | + import { browser } from '$app/environment'; |
2 | 3 | import type { Snippet } from 'svelte'; |
3 | 4 |
|
4 | 5 | type Props = { |
|
9 | 10 | }; |
10 | 11 |
|
11 | 12 | let { content, children, tooltipContent, interactive = false }: Props = $props(); |
| 13 | +
|
| 14 | + let wrapperEl = $state<HTMLElement | undefined>(); |
| 15 | + let tooltipEl = $state<HTMLElement | undefined>(); |
| 16 | + // Start off-screen so the first hover never flashes at a wrong position |
| 17 | + let posStyle = $state('left:-9999px;top:-9999px;'); |
| 18 | + let visible = $state(false); |
| 19 | + let hideTimer: ReturnType<typeof setTimeout> | undefined; |
| 20 | +
|
| 21 | + const GAP = 8; // gap between trigger and tooltip |
| 22 | + const MARGIN = 8; // minimum distance from viewport edges |
| 23 | +
|
| 24 | + function reposition() { |
| 25 | + if (!browser || !wrapperEl || !tooltipEl) return; |
| 26 | +
|
| 27 | + const trigger = wrapperEl.getBoundingClientRect(); |
| 28 | + const tw = tooltipEl.offsetWidth; |
| 29 | + const th = tooltipEl.offsetHeight; |
| 30 | + const vw = window.innerWidth; |
| 31 | +
|
| 32 | + // Center horizontally above the trigger, then clamp to viewport edges |
| 33 | + let x = trigger.left + trigger.width / 2 - tw / 2; |
| 34 | + x = Math.max(MARGIN, Math.min(x, vw - tw - MARGIN)); |
| 35 | +
|
| 36 | + // Position above the trigger |
| 37 | + const y = trigger.top - th - GAP; |
| 38 | +
|
| 39 | + posStyle = `left:${x}px;top:${y}px;`; |
| 40 | + } |
| 41 | +
|
| 42 | + function show() { |
| 43 | + clearTimeout(hideTimer); |
| 44 | + reposition(); |
| 45 | + visible = true; |
| 46 | + } |
| 47 | +
|
| 48 | + function scheduleHide() { |
| 49 | + if (interactive) { |
| 50 | + // Small delay lets the mouse travel from trigger to tooltip |
| 51 | + hideTimer = setTimeout(() => { visible = false; }, 100); |
| 52 | + } else { |
| 53 | + visible = false; |
| 54 | + } |
| 55 | + } |
| 56 | +
|
| 57 | + $effect(() => { |
| 58 | + if (!browser || !wrapperEl) return; |
| 59 | +
|
| 60 | + wrapperEl.addEventListener('mouseenter', show); |
| 61 | + wrapperEl.addEventListener('mouseleave', scheduleHide); |
| 62 | + wrapperEl.addEventListener('focusin', show); |
| 63 | + wrapperEl.addEventListener('focusout', scheduleHide); |
| 64 | +
|
| 65 | + return () => { |
| 66 | + wrapperEl!.removeEventListener('mouseenter', show); |
| 67 | + wrapperEl!.removeEventListener('mouseleave', scheduleHide); |
| 68 | + wrapperEl!.removeEventListener('focusin', show); |
| 69 | + wrapperEl!.removeEventListener('focusout', scheduleHide); |
| 70 | + }; |
| 71 | + }); |
| 72 | +
|
| 73 | + // Keep interactive tooltip open while the mouse is over it |
| 74 | + $effect(() => { |
| 75 | + if (!browser || !interactive || !tooltipEl) return; |
| 76 | +
|
| 77 | + tooltipEl.addEventListener('mouseenter', show); |
| 78 | + tooltipEl.addEventListener('mouseleave', scheduleHide); |
| 79 | +
|
| 80 | + return () => { |
| 81 | + tooltipEl!.removeEventListener('mouseenter', show); |
| 82 | + tooltipEl!.removeEventListener('mouseleave', scheduleHide); |
| 83 | + }; |
| 84 | + }); |
12 | 85 | </script> |
13 | 86 |
|
14 | | -<div class="tooltip-wrapper" tabindex="-1" role="presentation"> |
| 87 | +<div class="tooltip-wrapper" bind:this={wrapperEl} tabindex="-1" role="presentation"> |
15 | 88 | {@render children()} |
16 | | - <div class="tooltip" class:interactive role="tooltip"> |
| 89 | + <div |
| 90 | + class="tooltip" |
| 91 | + class:visible |
| 92 | + class:interactive |
| 93 | + role="tooltip" |
| 94 | + bind:this={tooltipEl} |
| 95 | + style={posStyle} |
| 96 | + > |
17 | 97 | {#if tooltipContent} |
18 | 98 | {@render tooltipContent()} |
19 | 99 | {:else if content} |
|
30 | 110 | } |
31 | 111 |
|
32 | 112 | .tooltip { |
33 | | - position: absolute; |
34 | | - bottom: 100%; |
35 | | - left: 50%; |
36 | | - transform: translateX(-50%); |
37 | | - margin-bottom: 8px; |
38 | | - padding: 0.75rem 1rem; |
| 113 | + /* Fixed escapes overflow:hidden on any ancestor (e.g. archive collapse container) |
| 114 | + and overflow-x:clip on html/body, so the tooltip is always fully visible. */ |
| 115 | + position: fixed; |
| 116 | + /* left / top set by JS; initial off-screen value prevents flash on first hover */ |
| 117 | + max-width: 260px; |
| 118 | + padding: 0.5rem 0.875rem; |
39 | 119 | background-color: var(--surface-three); |
40 | 120 | color: var(--text-four); |
41 | 121 | border-radius: 12px; |
42 | | - font-size: 16px; |
| 122 | + font-size: 0.875rem; |
43 | 123 | font-weight: 500; |
44 | | - white-space: nowrap; |
| 124 | + line-height: 1.5; |
| 125 | + text-align: center; |
| 126 | + white-space: normal; |
| 127 | + word-break: break-word; |
45 | 128 | box-shadow: var(--drop-shadow-one); |
46 | 129 | opacity: 0; |
47 | 130 | visibility: hidden; |
48 | 131 | transition: |
49 | 132 | opacity 0.2s ease, |
50 | 133 | visibility 0.2s ease; |
51 | | - z-index: var(--z-dropdown); |
| 134 | + z-index: var(--z-modal); |
52 | 135 | pointer-events: none; |
53 | 136 | } |
54 | 137 |
|
55 | | - .tooltip::after { |
56 | | - content: ''; |
57 | | - position: absolute; |
58 | | - top: 100%; |
59 | | - left: 0; |
60 | | - right: 0; |
61 | | - height: 12px; |
| 138 | + .tooltip.visible { |
| 139 | + opacity: 1; |
| 140 | + visibility: visible; |
62 | 141 | } |
63 | 142 |
|
64 | | - @media (hover: hover) { |
65 | | - .tooltip-wrapper:hover .tooltip { |
66 | | - opacity: 1; |
67 | | - visibility: visible; |
68 | | - } |
69 | | -
|
70 | | - .tooltip-wrapper:hover .tooltip.interactive { |
71 | | - pointer-events: auto; |
72 | | - } |
73 | | - } |
74 | | -
|
75 | | - @media (hover: none) { |
76 | | - .tooltip-wrapper:focus-within .tooltip { |
77 | | - opacity: 1; |
78 | | - visibility: visible; |
79 | | - } |
80 | | -
|
81 | | - .tooltip-wrapper:focus-within .tooltip.interactive { |
82 | | - pointer-events: auto; |
83 | | - } |
| 143 | + .tooltip.visible.interactive { |
| 144 | + pointer-events: auto; |
84 | 145 | } |
85 | 146 |
|
86 | 147 | .tooltip :global(a) { |
87 | 148 | color: var(--primary); |
88 | 149 | text-decoration: none; |
89 | | - font-size: 17px; |
| 150 | + font-size: 0.9375rem; |
90 | 151 | } |
91 | 152 |
|
92 | 153 | .tooltip :global(a:hover) { |
|
95 | 156 |
|
96 | 157 | .tooltip :global(p) { |
97 | 158 | margin: 0 0 0.25rem 0; |
98 | | - font-size: 17px; |
| 159 | + font-size: 0.9375rem; |
99 | 160 | color: var(--text-four); |
100 | 161 | opacity: 0.8; |
101 | 162 | } |
|
0 commit comments