Tooltip Flickering Bug Analysis
Overview
This document provides a detailed analysis of a tooltip flickering issue that occurred specifically on working session blocks in the timetable component's day view. The bug caused tooltips to rapidly show and hide when hovering over working session blocks, making them difficult to read, while planned task tooltips worked perfectly.
Bug Description
Symptoms
- Affected Elements: Working session blocks in the right column of the dual timetable view
 - Behavior: Tooltip would rapidly flicker (show/hide) when hovering over working session blocks
 - User Impact: Tooltips were nearly impossible to read due to constant flickering
 - Scope: Only affected working session blocks; planned task blocks (left column) worked normally
 
Environment
- Component: 
TimetableComponent(src/app/pages/timetable/timetable.component.ts) - View Mode: Day view with "both" mode (showing both planned tasks and working sessions)
 - Browser: All modern browsers
 - Framework: Angular with signals
 
Root Cause Analysis
Initial Misconceptions
Initially, we suspected the issue was related to:
- ❌ Tooltip positioning logic
 - ❌ Mouse event handling differences
 - ❌ Z-index conflicts
 - ❌ Pointer events interference
 
Actual Root Cause: CSS Hover Effect Feedback Loop
The real culprit was a feedback loop created by CSS hover effects combined with precise mouse event detection.
The Problematic CSS
Working Session Block Styles:
.working-session-block {
  opacity: 0.9;
  /* ... other styles */
}
.working-session-block:hover {
  opacity: 1;
}
.task-block:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
Planned Task Blocks:
.task-block:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* No additional opacity changes */
The Feedback Loop Mechanism
- 
Initial State: Working session block has
opacity: 0.9 - 
Mouse Enter:
- Mouse cursor enters the working session block
 mouseenterevent fires →showTooltip()called- CSS 
:hoveractivates →opacity: 1+transform: translateY(-1px) - Block visually shifts up by 1px and becomes more opaque
 
 - 
Critical Issue:
- The 
transform: translateY(-1px)physically moves the element - The mouse cursor, which was at the bottom edge of the element, is now technically "outside" the transformed element bounds
 - Browser detects this as the mouse "leaving" the element
 
 - The 
 - 
Mouse Leave:
mouseleaveevent fires →hideTooltip()called- CSS 
:hoverdeactivates → element returns to original position - Block moves back down 1px and opacity returns to 0.9
 
 - 
Mouse Re-enter:
- Mouse cursor is now "over" the element again (since it moved back to original position)
 mouseenterfires again → cycle repeats
 - 
Result: Rapid fire mouse enter/leave events creating the flickering effect
 
Why Planned Tasks Didn't Flicker
Planned task blocks only had the transform: translateY(-1px) hover effect without the opacity change. While they also moved slightly, the single transform was less likely to create the precise conditions needed for the feedback loop, and the visual change was more subtle.
Technical Deep Dive
Mouse Event Timing
Time: 0ms    - mouseenter → showTooltip() → opacity: 0.9→1, translateY(0→-1px)
Time: 1ms    - element moves, mouse is now "outside" bounds
Time: 2ms    - mouseleave → hideTooltip() → opacity: 1→0.9, translateY(-1px→0)
Time: 3ms    - element moves back, mouse is now "inside" bounds again
Time: 4ms    - mouseenter → showTooltip() → ...
Code Flow Analysis
Tooltip Event Handlers (Same for Both Types):
(mouseenter)="!isDragging() && showTooltip($event, block)"
(mousemove)="!isDragging() && moveTooltip($event)"
(mouseleave)="hideTooltip()"
Original showTooltip Method:
showTooltip(evt: MouseEvent, block: TimetableBlock) {
  this.tooltipBlock.set(block);
  this.tooltipVisible.set(true);
  this.positionTooltip(evt);
}
Original hideTooltip Method:
hideTooltip() {
  this.tooltipVisible.set(false);
  this.tooltipBlock.set(null);
}
The issue was that hideTooltip() executed immediately when mouseleave fired, with no delay to account for rapid re-entry.
Solution Implementation
Strategy: Debounced Tooltip Hiding
The solution implements a debounced hiding mechanism that:
- Immediately shows tooltips when 
mouseenteroccurs - Cancels any pending hide operations when re-entering
 - Delays hiding by 100ms to prevent rapid show/hide cycles
 
Code Changes
1. Added Hide Timeout Tracking
// floating tooltip state for day view
tooltipVisible = signal(false);
tooltipBlock = signal<TimetableBlock | null>(null);
tooltipX = signal(0);
tooltipY = signal(0);
private tooltipMoveTimeout?: number;
private tooltipHideTimeout?: number; // ← NEW
2. Enhanced showTooltip Method
showTooltip(evt: MouseEvent, block: TimetableBlock) {
  // Clear any pending hide timeout to prevent flicker
  if (this.tooltipHideTimeout) {
    clearTimeout(this.tooltipHideTimeout);
    this.tooltipHideTimeout = undefined;
  }
  
  this.tooltipBlock.set(block);
  this.tooltipVisible.set(true);
  this.positionTooltip(evt);
}
Key Addition: Canceling pending hide operations prevents the tooltip from hiding if the user quickly re-enters the element.
3. Implemented Debounced Hiding
hideTooltip() {
  // Clear any pending tooltip repositioning
  if (this.tooltipMoveTimeout) {
    clearTimeout(this.tooltipMoveTimeout);
    this.tooltipMoveTimeout = undefined;
  }
  
  // Add a small delay to prevent flickering caused by hover effects
  this.tooltipHideTimeout = window.setTimeout(() => {
    this.tooltipVisible.set(false);
    this.tooltipBlock.set(null);
    this.tooltipHideTimeout = undefined;
  }, 100); // 100ms delay to prevent rapid show/hide cycles
}
Key Addition: 100ms delay before actually hiding the tooltip, giving enough time for rapid re-entry to cancel the hide operation.
4. Updated Cleanup
ngOnDestroy() {
  if (this.timeUpdateInterval) {
    clearInterval(this.timeUpdateInterval);
  }
  
  // Clean up tooltip timeouts
  if (this.tooltipMoveTimeout) {
    clearTimeout(this.tooltipMoveTimeout);
  }
  if (this.tooltipHideTimeout) { // ← NEW
    clearTimeout(this.tooltipHideTimeout);
  }
}
Why This Solution Works
1. Immediate Show, Delayed Hide
- Tooltips appear instantly when needed
 - Hiding is delayed just enough to break the feedback loop
 - 100ms is imperceptible to users but sufficient to prevent rapid cycles
 
2. Smart Cancellation
- If user re-enters during the 100ms delay, the hide is cancelled
 - This handles legitimate quick mouse movements
 - Prevents "dead zones" where tooltips disappear inappropriately
 
3. Non-Breaking
- Solution doesn't affect planned task tooltips (they continue working as before)
 - No changes to positioning logic or visual styling
 - Maintains all existing functionality
 
4. Robust Edge Case Handling
- Proper cleanup prevents memory leaks
 - Handles rapid mouse movements gracefully
 - Works across all browsers and devices
 
Performance Considerations
Memory Impact
- Minimal: Only adds one additional timeout reference per component instance
 - Cleanup: Proper cleanup in 
ngOnDestroyprevents memory leaks 
CPU Impact
- Negligible: setTimeout operations are very lightweight
 - Optimization: Cancelling pending timeouts prevents unnecessary operations
 
User Experience Impact
- Positive: Eliminates jarring flickering effect
 - Imperceptible: 100ms delay is below human perception threshold for UI responsiveness
 - Improved: Tooltips are now reliably readable
 
Alternative Solutions Considered
1. ❌ Remove CSS Hover Effects
Pros: Would eliminate the root cause Cons: Would remove valuable visual feedback for users
2. ❌ Increase CSS Transition Delays
Pros: Might reduce the feedback loop frequency Cons: Would make the UI feel sluggish and wouldn't guarantee a fix
3. ❌ Use Pointer Events Instead of Mouse Events
Pros: Different event timing might avoid the issue Cons: Browser compatibility concerns and wouldn't address the fundamental CSS issue
4. ❌ Implement Mouse Position Tracking
Pros: Could detect rapid position changes Cons: Complex implementation, performance overhead, still wouldn't solve CSS feedback loop
5. ✅ Debounced Hiding (Chosen Solution)
Pros:
- Addresses root cause without removing features
 - Simple, reliable implementation
 - Maintains all existing functionality
 - Minimal performance impact
 
Testing Strategy
Test Cases Covered
- Working Session Tooltip Stability: Hover over working session blocks → tooltip should appear and remain stable
 - Planned Task Tooltip Unchanged: Hover over planned tasks → tooltips should work exactly as before
 - Rapid Mouse Movement: Quickly move mouse in/out of blocks → no flickering
 - Edge Cases: Move between adjacent blocks → tooltips should transition smoothly
 - Performance: Extended usage → no memory leaks or performance degradation
 
Browser Testing
- ✅ Chrome/Chromium
 - ✅ Firefox
 - ✅ Safari
 - ✅ Edge
 
Lessons Learned
1. CSS and JavaScript Interaction
CSS hover effects can create unexpected interactions with JavaScript event handling. Visual transformations need to be considered when implementing mouse-based interactions.
2. Event Timing is Critical
Mouse events fire with very precise timing. Even 1px movements can trigger enter/leave events, especially when elements are being transformed.
3. Debugging Interactive Issues
Visual debugging tools may not capture rapid flickering. Sometimes the solution requires understanding the precise sequence of events rather than just the visual symptoms.
4. Debouncing is Powerful
Small delays (100ms) can solve seemingly complex interaction issues without impacting user experience.
5. Defensive Programming
Always consider edge cases in UI interactions. What seems like a simple hover effect can create complex feedback loops.
Future Considerations
Potential Improvements
- Adaptive Delay: Could adjust the 100ms delay based on user's mouse movement speed
 - Intelligent Positioning: Could factor in CSS transforms when calculating tooltip positions
 - Global Tooltip Manager: For complex applications, a centralized tooltip service might prevent similar issues
 
Monitoring
Consider adding telemetry to track:
- Tooltip show/hide frequency
 - User interaction patterns
 - Performance metrics for tooltip operations
 
Conclusion
This bug demonstrates how seemingly simple UI interactions can create complex feedback loops when CSS animations and JavaScript event handling intersect. The solution successfully eliminates the flickering while maintaining all existing functionality through a simple but effective debouncing strategy.
The key insight was recognizing that the problem wasn't in the tooltip positioning logic itself, but in the timing of show/hide operations relative to CSS hover effects. This highlights the importance of considering the complete interaction chain when debugging UI issues.