Skip to content

fix show-more button#7415

Merged
akatsoulas merged 1 commit intomozilla:mainfrom
escattone:fix-show-more-2965
Apr 17, 2026
Merged

fix show-more button#7415
akatsoulas merged 1 commit intomozilla:mainfrom
escattone:fix-show-more-2965

Conversation

@escattone
Copy link
Copy Markdown
Contributor

@escattone escattone commented Apr 9, 2026

mozilla/sumo#2965

Here's the explanation from claude:

How the sidebar works

The sidebar tag filter (#sidebar-tag-filter) loads its content lazily via hx-trigger="load". Once loaded, initSidebarTagFilter() runs, queries the DOM for the button and tag items, attaches a click listener to .sidebar-tags--show-more, and sets container.dataset.initialized = "true" as a guard against duplicate initialization.

What happens when you click a tag

Each tag link has hx-push-url="true", which matters because of how htmx 1.9.12 orders operations internally. Looking at the htmx source (line 3618), htmx:pushedIntoHistory fires before selectAndSwap — meaning the URL is pushed before the question list is actually swapped.

The sequence:

  1. htmx:pushedIntoHistory fires — Our handler constructs a new sidebar URL and calls htmx.ajax("GET", newUrl, {target: sidebar, swap: "innerHTML" }). This starts an async request to reload the sidebar.
  2. Question list swaps — The original tag-click response replaces #questions-list. This triggers htmx:afterSettle where event.detail.target is #questions-list.
  3. Sidebar ajax completes — The sidebar's innerHTML is replaced with fresh HTML. The old .sidebar-tags--show-more button (and its click listener) is destroyed. A new button exists in the DOM but has no listener.
  4. Sidebar htmx:afterSettle fires — event.detail.target is the sidebar element.

The bug

The old code at step 4:

document.addEventListener("htmx:afterSettle", initSidebarTagFilter);

This calls initSidebarTagFilter(), which checks container.dataset.initialized. It's still "true" (set on the container element, which was never replaced — only its children were). So the function returns early at line 346. The new button never gets a listener.

The .then() callback on htmx.ajax() was supposed to clear the flag and re-initialize. But htmx 1.9.12 has a default settle delay of 20ms (setTimeout(doSettle, 20)) while the promise resolves synchronously in the XHR onload handler (line 3351). In the simple case this actually works (.then() runs before settle). But when requests get queued — e.g., the user clicks tags rapidly — htmx's sync strategy resolves the promise immediately at line 3166 without performing the swap. The .then() then re-initializes against stale DOM, and when the real swap finally happens, the listeners are destroyed with no recovery path.

The fix

Change 1 — Make the htmx:afterSettle handler aware of what was swapped:

  document.addEventListener("htmx:afterSettle", (event) => {
    const sidebar = document.getElementById("sidebar-tag-filter");
    if (sidebar && event.detail && event.detail.target === sidebar) {
      delete sidebar.dataset.initialized;
    }
    initSidebarTagFilter();
  });

When a question-list swap settles, target is #questions-list, so the flag stays put and initSidebarTagFilter() correctly returns early. When the sidebar swap settles, target is the sidebar itself, so we clear the flag and initSidebarTagFilter() runs fresh — querying the new DOM, attaching a listener to the new button.

Change 2 — Remove the .then() since the htmx:afterSettle handler now reliably handles re-initialization regardless of promise timing or request queuing.

@escattone escattone requested a review from akatsoulas April 9, 2026 18:06
@akatsoulas akatsoulas merged commit 2035b81 into mozilla:main Apr 17, 2026
6 checks passed
@escattone escattone deleted the fix-show-more-2965 branch April 17, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants