Skip to content

Commit d646458

Browse files
committed
fix: updating Tooltip positioning and styling to render Visible Tooltips
1 parent e3cddb9 commit d646458

File tree

2 files changed

+146
-116
lines changed

2 files changed

+146
-116
lines changed

web/src/lib/components/chat/SessionManager.svelte

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { chatState, clearMessages, revertLastMessage, currentSession, messageStore } from '$lib/store/chat-store';
66
import { Button } from '$lib/components/ui/button';
77
import { toastService } from '$lib/services/toast-service';
8+
import Tooltip from '$lib/components/ui/tooltip/Tooltip.svelte';
89
910
let sessionsList: string[] = [];
1011
$: sessionName = $currentSession;
@@ -16,15 +17,13 @@
1617
try {
1718
await sessionAPI.loadSessions();
1819
} catch (error) {
19-
console.error('Failed to load sessions:', error);
2020
}
2121
});
2222
2323
async function saveSession() {
2424
try {
2525
await sessionAPI.exportToFile($chatState.messages);
2626
} catch (error) {
27-
console.error('Failed to save session:', error);
2827
}
2928
}
3029
@@ -33,7 +32,6 @@
3332
const messages = await sessionAPI.importFromFile();
3433
messageStore.set(messages);
3534
} catch (error) {
36-
console.error('Failed to load session:', error);
3735
}
3836
}
3937
@@ -49,20 +47,34 @@
4947

5048
<div class="p-1 m-1 mr-2">
5149
<div class="flex gap-2">
52-
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
50+
<Tooltip text="Revert Last Message" position="top">
51+
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
5352
<RotateCcw class="h-4 w-4" />
54-
</Button>
55-
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
53+
</Button>
54+
</Tooltip>
55+
56+
<Tooltip text="Clear Chat" position="top">
57+
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
5658
<Trash2 class="h-4 w-4" />
57-
</Button>
58-
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
59+
</Button>
60+
</Tooltip>
61+
62+
<Tooltip text="Copy Chat to Clipboard" position="top">
63+
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
5964
<Copy class="h-4 w-4" />
60-
</Button>
61-
<Button variant="outline" size="icon" aria-label="Load Session" on:click={loadSession}>
65+
</Button>
66+
</Tooltip>
67+
68+
<Tooltip text="Load Session from File" position="top">
69+
<Button variant="outline" size="icon" aria-label="Load Session" on:click={loadSession}>
6270
<FileIcon class="h-4 w-4" />
63-
</Button>
64-
<Button variant="outline" size="icon" aria-label="Save Session" on:click={saveSession}>
71+
</Button>
72+
</Tooltip>
73+
74+
<Tooltip text="Save Session to File" position="top">
75+
<Button variant="outline" size="icon" aria-label="Save Session" on:click={saveSession}>
6576
<Save class="h-4 w-4" />
66-
</Button>
77+
</Button>
78+
</Tooltip>
6779
</div>
6880
</div>

web/src/lib/components/ui/tooltip/Tooltip.svelte

Lines changed: 121 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -4,116 +4,134 @@
44
55
let tooltipVisible = false;
66
let tooltipElement: HTMLDivElement;
7-
8-
function showTooltip() {
9-
tooltipVisible = true;
7+
let triggerElement: HTMLDivElement;
8+
let showTimeout: ReturnType<typeof setTimeout> | null = null;
9+
10+
function showTooltip() {
11+
// Clear any existing timeout
12+
if (showTimeout) {
13+
clearTimeout(showTimeout);
14+
}
15+
16+
// Set 500ms 0.5 second delay before showing tooltip (reduced for testing)
17+
showTimeout = setTimeout(() => {
18+
tooltipVisible = true;
19+
}, 500);
1020
}
1121
12-
function hideTooltip() {
22+
function hideTooltip() {
23+
// Clear the show timeout if user leaves before the set delay
24+
if (showTimeout) {
25+
clearTimeout(showTimeout);
26+
showTimeout = null;
27+
}
28+
1329
tooltipVisible = false;
1430
}
15-
</script>
16-
17-
<!-- svelte-ignore a11y-no-noninteractive-element-interactions a11y-mouse-events-have-key-events -->
18-
<div class="tooltip-container">
19-
<div
20-
class="tooltip-trigger"
21-
on:mouseenter={showTooltip}
22-
on:mouseleave={hideTooltip}
23-
on:focusin={showTooltip}
24-
on:focusout={hideTooltip}
25-
role="tooltip"
26-
aria-label="Tooltip trigger"
27-
>
28-
<slot />
29-
</div>
30-
31-
{#if tooltipVisible}
32-
<div
33-
bind:this={tooltipElement}
34-
class="tooltip absolute z-[9999] px-2 py-1 text-xs rounded bg-gray-900/90 text-white whitespace-nowrap shadow-lg backdrop-blur-sm"
35-
class:top="{position === 'top'}"
36-
class:bottom="{position === 'bottom'}"
37-
class:left="{position === 'left'}"
38-
class:right="{position === 'right'}"
39-
role="tooltip"
40-
aria-label={text}
41-
>
42-
{text}
43-
<div class="tooltip-arrow" role="presentation" />
44-
</div>
45-
{/if}
46-
</div>
47-
48-
<style>
49-
.tooltip-container {
50-
position: relative;
51-
display: inline-block;
52-
}
53-
54-
.tooltip-trigger {
55-
display: inline-flex;
56-
}
57-
58-
.tooltip {
59-
pointer-events: none;
60-
transition: all 150ms ease-in-out;
61-
opacity: 1;
62-
}
63-
64-
.tooltip.top {
65-
bottom: calc(100% + 5px);
66-
left: 50%;
67-
transform: translateX(-50%);
68-
}
6931
70-
.tooltip.bottom {
71-
top: calc(100% + 5px);
72-
left: 50%;
73-
transform: translateX(-50%);
32+
function updateTooltipPosition() {
33+
if (!tooltipVisible || !triggerElement || !tooltipElement) {
34+
return;
35+
}
36+
37+
const triggerRect = triggerElement.getBoundingClientRect();
38+
const tooltipRect = tooltipElement.getBoundingClientRect();
39+
const viewportWidth = window.innerWidth;
40+
const viewportHeight = window.innerHeight;
41+
42+
let top = 0;
43+
let left = 0;
44+
let actualPosition = position;
45+
46+
// Smart positioning - flip to opposite side if not enough space
47+
switch (position) {
48+
case 'top':
49+
// Check if there's enough space above
50+
if (triggerRect.top - tooltipRect.height - 8 < 8) {
51+
actualPosition = 'bottom';
52+
top = triggerRect.bottom + 8;
53+
} else {
54+
top = triggerRect.top - tooltipRect.height - 8;
55+
}
56+
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
57+
break;
58+
59+
case 'bottom':
60+
// Check if there's enough space below
61+
if (triggerRect.bottom + tooltipRect.height + 8 > viewportHeight - 8) {
62+
actualPosition = 'top';
63+
top = triggerRect.top - tooltipRect.height - 8;
64+
} else {
65+
top = triggerRect.bottom + 8;
66+
}
67+
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
68+
break;
69+
70+
case 'left':
71+
// Check if there's enough space to the left
72+
if (triggerRect.left - tooltipRect.width - 8 < 8) {
73+
actualPosition = 'right';
74+
left = triggerRect.right + 8;
75+
} else {
76+
left = triggerRect.left - tooltipRect.width - 8;
77+
}
78+
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
79+
break;
80+
81+
case 'right':
82+
// Check if there's enough space to the right
83+
if (triggerRect.right + tooltipRect.width + 8 > viewportWidth - 8) {
84+
actualPosition = 'left';
85+
left = triggerRect.left - tooltipRect.width - 8;
86+
} else {
87+
left = triggerRect.right + 8;
88+
}
89+
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
90+
break;
91+
}
92+
93+
// Final viewport boundary enforcement (fallback)
94+
if (left < 8) left = 8;
95+
if (left + tooltipRect.width > viewportWidth - 8) {
96+
left = viewportWidth - tooltipRect.width - 8;
97+
}
98+
if (top < 8) top = 8;
99+
if (top + tooltipRect.height > viewportHeight - 8) {
100+
top = viewportHeight - tooltipRect.height - 8;
101+
}
102+
103+
tooltipElement.style.top = `${top}px`;
104+
tooltipElement.style.left = `${left}px`;
74105
}
75106
76-
.tooltip.left {
77-
right: calc(100% + 5px);
78-
top: 50%;
79-
transform: translateY(-50%);
80-
}
81-
82-
.tooltip.right {
83-
left: calc(100% + 5px);
84-
top: 50%;
85-
transform: translateY(-50%);
86-
}
87-
88-
.tooltip-arrow {
89-
position: absolute;
90-
width: 8px;
91-
height: 8px;
92-
background: inherit;
93-
transform: rotate(45deg);
94-
}
95-
96-
.tooltip.top .tooltip-arrow {
97-
bottom: -4px;
98-
left: 50%;
99-
margin-left: -4px;
100-
}
101-
102-
.tooltip.bottom .tooltip-arrow {
103-
top: -4px;
104-
left: 50%;
105-
margin-left: -4px;
107+
// Update position when tooltip becomes visible
108+
$: if (tooltipVisible) {
109+
setTimeout(updateTooltipPosition, 0);
106110
}
111+
</script>
107112

108-
.tooltip.left .tooltip-arrow {
109-
right: -4px;
110-
top: 50%;
111-
margin-top: -4px;
112-
}
113+
<!-- Tooltip trigger container - no positioning constraints -->
114+
<div
115+
class="tooltip-trigger cursor-pointer"
116+
bind:this={triggerElement}
117+
on:mouseenter={showTooltip}
118+
on:mouseleave={hideTooltip}
119+
on:focusin={showTooltip}
120+
on:focusout={hideTooltip}
121+
role="tooltip"
122+
aria-label="Tooltip trigger"
123+
>
124+
<slot />
125+
</div>
113126

114-
.tooltip.right .tooltip-arrow {
115-
left: -4px;
116-
top: 50%;
117-
margin-top: -4px;
118-
}
119-
</style>
127+
<!-- Tooltip rendered outside normal flow -->
128+
{#if tooltipVisible}
129+
<div
130+
bind:this={tooltipElement}
131+
class="tooltip fixed z-[9999] px-3 py-2 text-sm rounded-lg bg-gray-500 text-white whitespace-nowrap shadow-xl border-2 border-gray-600"
132+
role="tooltip"
133+
aria-label={text}
134+
>
135+
{text}
136+
</div>
137+
{/if}

0 commit comments

Comments
 (0)