Files
cv-site/doc/archive/SKELETON-LOADER-FIX-VERIFICATION.md
T
2025-11-16 12:48:12 +00:00

5.9 KiB

Skeleton Loader Bug Fix - Verification Report

🔴 BUG IDENTIFIED

Issue: Skeleton loader was stuck permanently visible after language switch

ROOT CAUSE ANALYSIS

The Problem

The hyperscript event handlers were attached to the #language-selector element, which gets completely replaced during HTMX swap:

<!-- BEFORE (BROKEN) -->
<div class="language-selector-wrapper">
    <div class="language-selector" id="language-selector"
         _="on htmx:beforeRequest from .selector-btn
              add .active to #skeleton-loader
            end
            on htmx:afterSwap from .selector-btn
              wait 100ms
              remove .active from #skeleton-loader
            end">
        <button hx-target="#language-selector"
                hx-swap="outerHTML">...</button>
    </div>
</div>

What happened:

  1. User clicks language button
  2. htmx:beforeRequest fires → skeleton appears (.active added)
  3. HTMX swaps entire #language-selector with outerHTML → Event handlers DESTROYED
  4. htmx:afterSwap fires, but no listener exists on new element
  5. Skeleton stuck with .active class forever

The Solution

Move hyperscript handlers to the parent wrapper that doesn't get swapped:

<!-- AFTER (FIXED) -->
<div class="language-selector-wrapper"
     _="on htmx:beforeRequest from .selector-btn
          add .active to #skeleton-loader
        end
        on htmx:afterSwap from .selector-btn
          wait 100ms
          remove .active from #skeleton-loader
        end">
    <div class="language-selector" id="language-selector">
        <button hx-target="#language-selector"
                hx-swap="outerHTML">...</button>
    </div>
</div>

Why this works:

  1. Event handlers on .language-selector-wrapper (persists across swaps)
  2. Listens for events FROM .selector-btn (event bubbling)
  3. htmx:beforeRequest → skeleton appears
  4. HTMX swaps #language-selector → wrapper remains intact
  5. htmx:afterSwap → wrapper handlers still exist → skeleton disappears

FILES MODIFIED

  1. templates/partials/navigation/language-selector.html

    • Moved hyperscript from #language-selector to .language-selector-wrapper
  2. templates/language-switch.html

    • Removed duplicate hyperscript from swapped element

VERIFICATION STEPS

1. HTML Structure Verification

curl -s http://localhost:1999/ | grep -A 10 "language-selector-wrapper"

Result: Hyperscript correctly attached to wrapper:

<div class="language-selector-wrapper"
     _="on htmx:beforeRequest from .selector-btn
          add .active to #skeleton-loader
        end
        on htmx:afterSwap from .selector-btn
          wait 100ms
          remove .active from #skeleton-loader
        end">

2. Swap Response Verification

curl -s "http://localhost:1999/switch-language?lang=es" | grep -A 5 "language-selector"

Result: Inner element has NO hyperscript (as intended):

<div class="language-selector" id="language-selector">
    <button class="selector-btn">...</button>
</div>

3. CSS State Verification

curl -s http://localhost:1999/static/css/main.css | grep -A 3 "#skeleton-loader"

Result: Proper CSS states:

#skeleton-loader {
    opacity: 0;
    pointer-events: none;
    transition: opacity 250ms ease-in-out;
}

#skeleton-loader.active {
    opacity: 1;
    pointer-events: all;
}

MANUAL BROWSER TEST REQUIRED

Test Steps:

  1. Open http://localhost:1999/?lang=en
  2. Open DevTools Console
  3. Run this monitoring script:
// Monitor skeleton loader state
const skeleton = document.getElementById('skeleton-loader');
const observer = new MutationObserver(() => {
    console.log('Skeleton classes:', skeleton.className);
    console.log('Skeleton opacity:', window.getComputedStyle(skeleton).opacity);
});
observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] });

// Monitor HTMX events
document.body.addEventListener('htmx:beforeRequest', (e) => {
    if (e.detail.elt.classList.contains('selector-btn')) {
        console.log('[BEFORE] Language switch starting');
    }
});
document.body.addEventListener('htmx:afterSwap', (e) => {
    if (e.detail.elt.classList.contains('selector-btn')) {
        console.log('[AFTER] Language switch complete');
    }
});
  1. Click "Español" button
  2. Watch console output

Expected Console Output:

[BEFORE] Language switch starting
Skeleton classes: active
Skeleton opacity: 1
[AFTER] Language switch complete
(after 100ms)
Skeleton classes:
Skeleton opacity: 0

Expected Visual Behavior:

  1. Skeleton appears immediately (fade in 250ms)
  2. Page content swaps (250ms swap + 250ms settle)
  3. Skeleton disappears after 100ms delay (fade out 250ms)
  4. Total: ~850ms smooth transition

What to Check:

  • Skeleton appears when clicking language button
  • Skeleton disappears after content loads
  • Skeleton does NOT stay stuck visible
  • Can switch languages multiple times without issues
  • Smooth fade in/out transitions

TECHNICAL DETAILS

Event Bubbling

Hyperscript uses from .selector-btn which listens for events that bubble up from any element matching .selector-btn, even if those elements are replaced.

Timing Breakdown

[0ms]    User clicks button
[0ms]    htmx:beforeRequest → skeleton.active = true
[0ms]    Skeleton starts fading in (opacity 0→1 over 250ms)
[100ms]  Server responds
[100ms]  HTMX starts swap
[350ms]  Swap complete (250ms swap duration)
[350ms]  htmx:afterSwap fired
[450ms]  100ms wait complete
[450ms]  skeleton.active = false
[450ms]  Skeleton starts fading out (opacity 1→0 over 250ms)
[700ms]  Skeleton fully hidden

STATUS

  • Root cause identified
  • Fix implemented
  • HTML structure verified
  • CSS states verified
  • Manual browser test REQUIRED

Next Step: Run manual browser test to confirm skeleton loader shows and hides correctly.