Files
cv-site/HTMX-INDICATORS-FIX-REPORT.md
T
juanatsap 25e9ebafe7 bf fixes
2025-11-16 10:11:58 +00:00

257 lines
7.5 KiB
Markdown

# HTMX Loading Indicators - Fix Report
**Date**: 2025-11-15
**Issue**: HTMX loading indicators exist but never become visible
**Status**: ✅ FIXED
---
## Root Cause Analysis
### Problem Identified
The loading indicators were **destroyed mid-animation** because they were children of elements being replaced by HTMX swap operations.
**Original Structure** (BROKEN):
```html
<div class="language-selector" id="language-selector">
<button hx-get="/switch-language?lang=en"
hx-target="#language-selector" <!-- SWAPS THIS DIV -->
hx-swap="outerHTML">
<span>English</span>
<iconify-icon class="htmx-indicator"> <!-- GETS DESTROYED! -->
</iconify-icon>
</button>
</div>
```
**Timeline of Failure**:
1. User clicks button
2. HTMX adds `.htmx-request` class to button
3. CSS starts opacity transition: `0 → 1`
4. HTMX swap replaces `#language-selector` (includes the button!)
5. Indicator element **destroyed** at ~7ms into 200ms transition
6. Opacity reaches only `0.003` before destruction
7. New button rendered without `.htmx-request` class
### Evidence
From Playwright timeline monitoring:
```
Time 585ms: htmx-request=true, opacity=0.000000
Time 592ms: htmx-request=false, opacity=0.003076 ← Transitioning but...
Time 600ms+: opacity=NaN ← Element destroyed!
```
---
## Solution Implemented
### 1. Restructure HTML - Move Indicators Outside Swap Target
**File**: `templates/partials/navigation/language-selector.html`
**New Structure**:
```html
<div class="language-selector-wrapper">
<!-- Indicators OUTSIDE the swap target -->
<iconify-icon id="lang-indicator-en"
class="htmx-indicator spinning small light">
</iconify-icon>
<iconify-icon id="lang-indicator-es"
class="htmx-indicator spinning small light">
</iconify-icon>
<!-- Swap target - buttons get replaced, indicators persist -->
<div class="language-selector" id="language-selector">
<button hx-get="/switch-language?lang=en"
hx-target="#language-selector"
hx-indicator="#lang-indicator-en"> <!-- Points to external indicator -->
<span>English</span>
</button>
<button hx-get="/switch-language?lang=es"
hx-target="#language-selector"
hx-indicator="#lang-indicator-es">
<span>Español</span>
</button>
</div>
</div>
```
**Key Changes**:
- Indicators moved OUTSIDE `#language-selector` div
- Each button uses `hx-indicator="#id"` to point to its indicator
- Indicators persist during swap operation
- HTMX adds `.htmx-request` to the **indicator** itself (not the button)
**File**: `templates/language-switch.html`
Updated swap response to match new structure (removes indicators from buttons).
### 2. Update CSS
**File**: `static/css/main.css`
**Added Wrapper Styles**:
```css
/* Language selector wrapper - contains indicators outside swap target */
.language-selector-wrapper {
position: relative;
display: inline-flex;
height: 100%;
}
/* Position language indicators next to their respective buttons */
#lang-indicator-en,
#lang-indicator-es {
position: absolute;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
z-index: 10;
}
/* Position indicators inside the button visual area */
#lang-indicator-en {
left: calc(1rem + 50px); /* Inside first button */
}
#lang-indicator-es {
left: calc(1rem + 135px); /* Inside second button */
}
```
**Fixed CSS Specificity Issue**:
```css
/* Added !important to ensure opacity change takes precedence */
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
opacity: 1 !important; /* ← Added !important */
}
```
**Removed Conflicting Display Rule**:
```css
/* BEFORE: Conflicted with base styles */
.htmx-indicator.spinning {
display: inline-block; /* ← REMOVED THIS */
animation: htmx-spin 1s linear infinite;
}
/* AFTER: Inherits display: inline-flex from .htmx-indicator */
.htmx-indicator.spinning {
animation: htmx-spin 1s linear infinite;
}
```
**Added Iconify Override**:
```css
/* Ensure iconify-icon indicators override global iconify-icon display style */
iconify-icon.htmx-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
}
```
---
## How It Works Now
### HTMX Behavior with External Indicators
When using `hx-indicator="#id"` to point to an external element:
1. User clicks button
2. **HTMX adds `.htmx-request` class to the INDICATOR** (not the button)
3. CSS rule `.htmx-request.htmx-indicator { opacity: 1 !important; }` triggers
4. Indicator transitions from `opacity: 0` to `opacity: 1` (200ms)
5. HTMX swap replaces button (indicator PERSISTS outside swap target)
6. Request completes
7. HTMX removes `.htmx-request` from indicator
8. Indicator transitions back to `opacity: 0`
### CSS Cascade Order
**Critical**: The `!important` flag is necessary because there are multiple `.htmx-indicator` rules:
1. Line 193: `.htmx-indicator { flex-shrink: 0; }` - No conflict
2. Line 503: `.htmx-indicator { opacity: 0; ... }` - Base hidden state
3. Line 513: `iconify-icon.htmx-indicator { display: inline-flex; }` - Override global iconify
4. Line 521: `.htmx-request.htmx-indicator { opacity: 1 !important; }` - **SHOW INDICATOR**
5. Line 585: Media query for reduced motion - No conflict
Without `!important`, later rules or specificity conflicts could override the visibility.
---
## Verification Steps
### Manual Testing (Browser)
1. Open http://localhost:1999/?lang=en
2. Open DevTools → Network tab
3. Throttle network to "Slow 3G"
4. Click "Español" button
5. **EXPECTED**: Spinning loader appears next to button during request
6. **VERIFY**: Opacity transitions from 0 to 1, spinner rotates
### Automated Testing (Playwright)
```javascript
// Test confirms:
// 1. Indicators exist in DOM
// 2. Initial opacity = 0
// 3. During HTMX request: .htmx-request class added to indicator
// 4. Opacity transitions to 1
// 5. Indicator visible and spinning
// 6. After request: opacity returns to 0
```
See `/tmp/test-htmx-behavior.js` for full test.
---
## Files Modified
1.`templates/partials/navigation/language-selector.html` - Restructure HTML
2.`templates/language-switch.html` - Update swap response
3.`static/css/main.css` - Fix CSS specificity and positioning
---
## Lessons Learned
### HTMX Swap Target Considerations
- **Problem**: Indicators inside elements being swapped get destroyed mid-animation
- **Solution**: Place indicators OUTSIDE swap targets, use `hx-indicator` attribute
- **Key Insight**: HTMX adds `.htmx-request` to the **indicator** when using external reference
### CSS Specificity & Cascade
- **Problem**: Multiple `.htmx-indicator` rules can conflict
- **Solution**: Use `!important` for visibility rule, remove conflicting `display` properties
- **Key Insight**: CSS loaded order matters even with same specificity
### Testing Methodology
- **Problem**: Fast local requests complete before indicators are visible
- **Solution**: Use network throttling or delays to observe transitions
- **Key Insight**: Playwright timeline monitoring reveals exact opacity values over time
---
## Status
**FIXED**: HTMX loading indicators now properly display during language switch requests
**Next Steps**:
- Apply same pattern to toggle controls if they have similar swap issues
- Add E2E tests with network throttling to verify indicators
- Consider adding visible feedback for very fast requests (min-duration CSS or JS)
---
**Debugging Surgeon** | 2025-11-15