Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/generate_changelog/incoming/1789.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### PR [#1789](https://github.com/danielmiessler/Fabric/pull/1789) by [JasonToups](https://github.com/JasonToups): Bug #1790: Tooltips Not rendering / Fix: Tooltip Update and added to Chat UI

- Fix: updating Tooltip positioning and styling to render Visible Tooltips
38 changes: 25 additions & 13 deletions web/src/lib/components/chat/SessionManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { chatState, clearMessages, revertLastMessage, currentSession, messageStore } from '$lib/store/chat-store';
import { Button } from '$lib/components/ui/button';
import { toastService } from '$lib/services/toast-service';
import Tooltip from '$lib/components/ui/tooltip/Tooltip.svelte';

let sessionsList: string[] = [];
$: sessionName = $currentSession;
Expand All @@ -16,15 +17,13 @@
try {
await sessionAPI.loadSessions();
} catch (error) {
console.error('Failed to load sessions:', error);
}
});

async function saveSession() {
try {
await sessionAPI.exportToFile($chatState.messages);
} catch (error) {
console.error('Failed to save session:', error);
}
}

Expand All @@ -33,7 +32,6 @@
const messages = await sessionAPI.importFromFile();
messageStore.set(messages);
} catch (error) {
console.error('Failed to load session:', error);
}
}

Expand All @@ -49,20 +47,34 @@

<div class="p-1 m-1 mr-2">
<div class="flex gap-2">
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
<Tooltip text="Revert Last Message" position="top">
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
<RotateCcw class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
</Button>
</Tooltip>

<Tooltip text="Clear Chat" position="top">
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
<Trash2 class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
</Button>
</Tooltip>

<Tooltip text="Copy Chat to Clipboard" position="top">
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
<Copy class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Load Session" on:click={loadSession}>
</Button>
</Tooltip>

<Tooltip text="Load Session from File" position="top">
<Button variant="outline" size="icon" aria-label="Load Session" on:click={loadSession}>
<FileIcon class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" aria-label="Save Session" on:click={saveSession}>
</Button>
</Tooltip>

<Tooltip text="Save Session to File" position="top">
<Button variant="outline" size="icon" aria-label="Save Session" on:click={saveSession}>
<Save class="h-4 w-4" />
</Button>
</Button>
</Tooltip>
</div>
</div>
224 changes: 121 additions & 103 deletions web/src/lib/components/ui/tooltip/Tooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,116 +4,134 @@

let tooltipVisible = false;
let tooltipElement: HTMLDivElement;

function showTooltip() {
tooltipVisible = true;
let triggerElement: HTMLDivElement;
let showTimeout: ReturnType<typeof setTimeout> | null = null;

function showTooltip() {
// Clear any existing timeout
if (showTimeout) {
clearTimeout(showTimeout);
}

// Set 500ms 0.5 second delay before showing tooltip (reduced for testing)
showTimeout = setTimeout(() => {
tooltipVisible = true;
}, 500);
}

function hideTooltip() {
function hideTooltip() {
// Clear the show timeout if user leaves before the set delay
if (showTimeout) {
clearTimeout(showTimeout);
showTimeout = null;
}

tooltipVisible = false;
}
</script>

<!-- svelte-ignore a11y-no-noninteractive-element-interactions a11y-mouse-events-have-key-events -->
<div class="tooltip-container">
<div
class="tooltip-trigger"
on:mouseenter={showTooltip}
on:mouseleave={hideTooltip}
on:focusin={showTooltip}
on:focusout={hideTooltip}
role="tooltip"
aria-label="Tooltip trigger"
>
<slot />
</div>

{#if tooltipVisible}
<div
bind:this={tooltipElement}
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"
class:top="{position === 'top'}"
class:bottom="{position === 'bottom'}"
class:left="{position === 'left'}"
class:right="{position === 'right'}"
role="tooltip"
aria-label={text}
>
{text}
<div class="tooltip-arrow" role="presentation" />
</div>
{/if}
</div>

<style>
.tooltip-container {
position: relative;
display: inline-block;
}

.tooltip-trigger {
display: inline-flex;
}

.tooltip {
pointer-events: none;
transition: all 150ms ease-in-out;
opacity: 1;
}

.tooltip.top {
bottom: calc(100% + 5px);
left: 50%;
transform: translateX(-50%);
}

.tooltip.bottom {
top: calc(100% + 5px);
left: 50%;
transform: translateX(-50%);
function updateTooltipPosition() {
if (!tooltipVisible || !triggerElement || !tooltipElement) {
return;
}

const triggerRect = triggerElement.getBoundingClientRect();
const tooltipRect = tooltipElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

let top = 0;
let left = 0;
let actualPosition = position;

// Smart positioning - flip to opposite side if not enough space
switch (position) {
case 'top':
// Check if there's enough space above
if (triggerRect.top - tooltipRect.height - 8 < 8) {
actualPosition = 'bottom';
top = triggerRect.bottom + 8;
} else {
top = triggerRect.top - tooltipRect.height - 8;
}
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
break;

case 'bottom':
// Check if there's enough space below
if (triggerRect.bottom + tooltipRect.height + 8 > viewportHeight - 8) {
actualPosition = 'top';
top = triggerRect.top - tooltipRect.height - 8;
} else {
top = triggerRect.bottom + 8;
}
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
break;

case 'left':
// Check if there's enough space to the left
if (triggerRect.left - tooltipRect.width - 8 < 8) {
actualPosition = 'right';
left = triggerRect.right + 8;
} else {
left = triggerRect.left - tooltipRect.width - 8;
}
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
break;

case 'right':
// Check if there's enough space to the right
if (triggerRect.right + tooltipRect.width + 8 > viewportWidth - 8) {
actualPosition = 'left';
left = triggerRect.left - tooltipRect.width - 8;
} else {
left = triggerRect.right + 8;
}
top = triggerRect.top + (triggerRect.height / 2) - (tooltipRect.height / 2);
break;
}

// Final viewport boundary enforcement (fallback)
if (left < 8) left = 8;
if (left + tooltipRect.width > viewportWidth - 8) {
left = viewportWidth - tooltipRect.width - 8;
}
if (top < 8) top = 8;
if (top + tooltipRect.height > viewportHeight - 8) {
top = viewportHeight - tooltipRect.height - 8;
}

tooltipElement.style.top = `${top}px`;
tooltipElement.style.left = `${left}px`;
}

.tooltip.left {
right: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
}

.tooltip.right {
left: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
}

.tooltip-arrow {
position: absolute;
width: 8px;
height: 8px;
background: inherit;
transform: rotate(45deg);
}

.tooltip.top .tooltip-arrow {
bottom: -4px;
left: 50%;
margin-left: -4px;
}

.tooltip.bottom .tooltip-arrow {
top: -4px;
left: 50%;
margin-left: -4px;
// Update position when tooltip becomes visible
$: if (tooltipVisible) {
setTimeout(updateTooltipPosition, 0);
}
</script>

.tooltip.left .tooltip-arrow {
right: -4px;
top: 50%;
margin-top: -4px;
}
<!-- Tooltip trigger container - no positioning constraints -->
<div
class="tooltip-trigger cursor-pointer"
bind:this={triggerElement}
on:mouseenter={showTooltip}
on:mouseleave={hideTooltip}
on:focusin={showTooltip}
on:focusout={hideTooltip}
role="tooltip"
aria-label="Tooltip trigger"
>
<slot />
</div>

.tooltip.right .tooltip-arrow {
left: -4px;
top: 50%;
margin-top: -4px;
}
</style>
<!-- Tooltip rendered outside normal flow -->
{#if tooltipVisible}
<div
bind:this={tooltipElement}
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"
role="tooltip"
aria-label={text}
>
{text}
</div>
{/if}