257 lines
7.5 KiB
Markdown
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
|