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