tooltip.svelte RawHistoryFolder
1<script lang="ts">
2 type $$Props = {
3 data: {
4 text: string,
5 direction?: 'top' | 'bottom' | 'left' | 'right'
6 }
7 };
8
9 let { children, data } = $props();
10
11 // Derive values from data with defaults
12 let text = $derived(data.text);
13 let direction = $derived(data.direction ?? 'top');
14
15 let wrapper: HTMLButtonElement;
16 let tooltipDiv = $state<HTMLDivElement | null>(null);
17 let coords = { x: 0, y: 0 };
18
19 function updateCoords() {
20 if (wrapper) {
21 const rect = wrapper.getBoundingClientRect();
22 if (direction === 'top') {
23 coords = { x: rect.left + rect.width / 2, y: rect.top - 8 };
24 } else if (direction === 'bottom') {
25 coords = { x: rect.left + rect.width / 2, y: rect.bottom + 8 };
26 } else if (direction === 'left') {
27 coords = { x: rect.left - 8, y: rect.top + rect.height / 2 };
28 } else if (direction === 'right') {
29 coords = { x: rect.right + 8, y: rect.top + rect.height / 2 };
30 }
31 }
32 }
33
34 function showTooltip() {
35 updateCoords();
36 if (tooltipDiv) return; // Prevent multiple tooltips
37 tooltipDiv = document.createElement('div');
38 tooltipDiv.className = `tooltip ${direction}`;
39 tooltipDiv.style.position = 'fixed';
40 tooltipDiv.style.zIndex = '1000000';
41 tooltipDiv.textContent = text;
42 tooltipDiv.setAttribute('role', 'tooltip');
43 tooltipDiv.setAttribute('aria-hidden', 'false');
44 document.body.appendChild(tooltipDiv);
45
46 const tooltipRect = tooltipDiv.getBoundingClientRect();
47 let finalLeft = coords.x;
48 let finalTop = coords.y;
49 if (direction === 'top') {
50 finalLeft = coords.x - tooltipRect.width / 2;
51 finalTop = coords.y - tooltipRect.height;
52 } else if (direction === 'bottom') {
53 finalLeft = coords.x - tooltipRect.width / 2;
54 finalTop = coords.y;
55 } else if (direction === 'left') {
56 finalLeft = coords.x - tooltipRect.width;
57 finalTop = coords.y - tooltipRect.height / 2;
58 } else if (direction === 'right') {
59 finalLeft = coords.x;
60 finalTop = coords.y - tooltipRect.height / 2;
61 }
62
63 if (finalLeft < 0) {
64 finalLeft = 0;
65 } else if (finalLeft + tooltipRect.width > window.innerWidth) {
66 finalLeft = window.innerWidth - tooltipRect.width;
67 }
68 if (finalTop < 0) {
69 finalTop = 0;
70 } else if (finalTop + tooltipRect.height > window.innerHeight) {
71 finalTop = window.innerHeight - tooltipRect.height;
72 }
73
74 tooltipDiv.style.left = `${finalLeft}px`;
75 tooltipDiv.style.top = `${finalTop}px`;
76 }
77
78 function hideTooltip() {
79 if (tooltipDiv && tooltipDiv.parentNode) {
80 tooltipDiv.parentNode.removeChild(tooltipDiv);
81 tooltipDiv = null;
82 }
83 }
84
85 function handleKeyDown(e: KeyboardEvent) {
86 if (e.key === 'Escape') {
87 hideTooltip();
88 }
89 }
90</script>
91
92<button
93 class="tooltip-wrapper"
94 bind:this={wrapper}
95 onmouseover={showTooltip}
96 onmouseout={hideTooltip}
97 onfocus={showTooltip}
98 onblur={hideTooltip}
99 onkeydown={handleKeyDown}
100 type="button"
101 aria-describedby={tooltipDiv?.id}
102>
103 {@render children()}
104</button>
105
106<style>
107 .tooltip-wrapper {
108 display: inline;
109 width: auto;
110 min-width: 0;
111 background: none !important;
112 border: none !important;
113 padding: 0 !important;
114 margin: 0 !important;
115 font: inherit !important;
116 color: inherit !important;
117 cursor: inherit !important;
118 text-align: inherit !important;
119 box-shadow: none !important;
120 text-shadow: inherit !important;
121 border-radius: 0 !important;
122 -webkit-appearance: none !important;
123 appearance: none !important;
124 white-space: pre;
125 }
126 .tooltip-wrapper:hover,
127 .tooltip-wrapper:active {
128 background: none !important;
129 border: none !important;
130 box-shadow: none !important;
131 }
132 .tooltip-wrapper:focus {
133 outline: none !important;
134 box-shadow: none !important;
135 }
136 .tooltip-wrapper:focus-visible {
137 outline: 2px solid #003C74 !important;
138 outline-offset: 2px !important;
139 }
140 /* Use :global for styles that need to apply to elements outside the component */
141 :global(.tooltip) {
142 white-space: nowrap;
143 padding: 0.5rem;
144 background: #ECE9D8;
145 color: #000;
146 border: 1px solid #003C74;
147 box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
148 border-radius: 2px;
149 font-size: 0.875rem;
150 font-family: 'Microsoft Sans Serif', 'Segoe UI', Arial, Tahoma, sans-serif;
151 }
152
153 /* Direction-specific styles for the tooltip arrow */
154 :global(.tooltip.top)::before {
155 content: '';
156 position: absolute;
157 bottom: -6px;
158 left: 50%;
159 transform: translateX(-50%);
160 border-left: 6px solid transparent;
161 border-right: 6px solid transparent;
162 border-top: 6px solid #003C74;
163 }
164
165 :global(.tooltip.bottom)::before {
166 content: '';
167 position: absolute;
168 top: -6px;
169 left: 50%;
170 transform: translateX(-50%);
171 border-left: 6px solid transparent;
172 border-right: 6px solid transparent;
173 border-bottom: 6px solid #003C74;
174 }
175
176 :global(.tooltip.left)::before {
177 content: '';
178 position: absolute;
179 right: -6px;
180 top: 50%;
181 transform: translateY(-50%);
182 border-top: 6px solid transparent;
183 border-bottom: 6px solid transparent;
184 border-left: 6px solid #003C74;
185 }
186
187 :global(.tooltip.right)::before {
188 content: '';
189 position: absolute;
190 left: -6px;
191 top: 50%;
192 transform: translateY(-50%);
193 border-top: 6px solid transparent;
194 border-bottom: 6px solid transparent;
195 border-right: 6px solid #003C74;
196 }
197</style>