Skip to content

fix: post-sweep follow-ups — prepend scroll under animate-leave, dev-server parity, ADR-002, docs-site loops callout - #326

Merged
ErickXavier merged 14 commits into
mainfrom
fix/followups-2026-08
Aug 4, 2026
Merged

fix: post-sweep follow-ups — prepend scroll under animate-leave, dev-server parity, ADR-002, docs-site loops callout#326
ErickXavier merged 14 commits into
mainfrom
fix/followups-2026-08

Conversation

@ErickXavier

Copy link
Copy Markdown
Collaborator

Post-sweep follow-ups

Refs #316 #317 #318 #319

Gate (merged branch)

  • Jest 4453/4453 (84 suites) green; e2e 521 passed / 46 expected skips / 0 failed ×3 browsers
  • gzip 44,105 B ≤ 44,500; committed dist matches fresh build
  • Locale parity: 1483 keys ×5 locales, zero drift; both dev/test servers boot-verified
  • No attribution trailers

Related but out of scope (tracked): #322, #324, #325, nojs-skill#74. Companion docs PR: no-js-dev/nojs-skill#73 (held).

ErickXavier and others added 14 commits August 4, 2026 15:27
Mirror the test-server.js pattern: env override before sibling
fallback, fs.stat graceful 404 when the Elements build is absent.

Refs #317
Establish docs/adr/ convention. Three decisions shipped in PRs
#308/#304: insert-mode single-source rendering, IntersectionObserver
root = nearest scrollable ancestor, and i18n post-init guard.

Refs #319
Surface the loops "Common Mistakes" content (directive on container vs
item) on the docs website. Adds the section with wrong/right code
examples for lists and tables, plus a rule-of-thumb callout.

All user-facing text uses t= i18n keys under docs.loops.commonMistakes
with translations in all 5 locales (en, pt, es, fr, it).

Refs #318
Drop "establishes the convention" — docs/adr/ already existed
with ADR-001. Fix post-init guard line range: 528-534, not 530-535.
…te-leave

When a loop has animate-leave, loops.js defers renderItems() behind
animationend/setTimeout. The prepend scroll compensation read a zero
scrollHeight delta and silently skipped adjustment, causing a visible
scroll jump when the deferred render landed.

Fall back to a MutationObserver on el that watches for the deferred
render's childList changes and compensates once the scrollHeight delta
becomes non-zero. The observer disconnects on height change or tree
disposal (el.__disposers), with an el.isConnected guard.

Refs #316
Replace curly/smart quotes with straight quotes in the
ruleOfThumb i18n value across all 5 locale files (en, pt, es,
fr, it) to match the codebase's established convention.

Refs #318
docs(site): add Common Mistakes section to loops page
fix(http): prepend scroll compensation under animate-leave
"la unidad que se repete" → "se repite"
…branch

The assignment at the end of the `if (isInsertMode && !_isFirstFetch)`
block is unreachable-as-meaningful: the guard already requires
_isFirstFetch to be false. The real transition from true to false
happens in the replace-mode path.
Replace the MutationObserver fallback for deferred scroll compensation
with an internal signal from loops.js. This eliminates four confirmed
design defects in the original mechanism from PR #323:

1. Stale/accumulating observers: zero-delta sync renders no longer arm
   any mechanism — the signal is only set when loops.js actually defers
   renderItems() behind animate-leave.
2. Fetch-time scrollTop restored: compensation is now delta-based at
   fire time (scrollTop += delta) instead of restoring a stale absolute
   position captured before the animation window.
3. Unrelated mutations mis-trigger: no MutationObserver means unrelated
   childList changes (nested if/show, loading indicators) cannot fire
   premature or wrong adjustments.
4. get+each combo leak: el.isConnected guard skips compensation when
   the loop element is detached (self-repeating pattern), preventing
   observers on detached nodes and unreachable __disposers entries.

Mechanism: loops.js sets parent._deferredRenderCallback = null when it
defers renderItems behind animate-leave, and invokes/clears the
callback after renderItems completes. http.js checks the signal after
$set — if present, registers a delta-based compensation callback; if
absent, compensates immediately from the measured delta.

Tests 68-73 cover: zero-delta sync with no stale interference, deferred
render compensation, user scroll preservation during deferral, unrelated
DOM mutation immunity, disposal mid-defer cleanup, and get+each combo
safety.
Addresses review feedback on PR #327:

1. BLOCKER — nesting: with <div get><ul><li each>, the signal landed
   on <ul> (loop parent) but http.js checked <div> (get-host).
   Fix: http.js pre-marks el with a _deferredRenderSlot object before
   $set; loops.js walks UP the ancestor chain from its parent to find
   the nearest ancestor with the slot. This bridges any nesting depth.
   Plain loops without an http.js ancestor pay only the walk cost
   (no slot found → no-op).

2. SHOULD — overlapping cycles: slot object identity serves as a
   generation guard. A new prepend cycle creates a new slot object;
   loops.js checks identity before invoking — if the slot was replaced
   by a newer cycle, the stale callback no-ops.

3. SHOULD — dead guard: removed the unreachable !el.isConnected branch
   (the _isLoopElement guard in http.js init bails before the prepend
   code is ever reached for get+each combo).

4. NIT — restored the rationale comment for pushing directly to
   el.__disposers instead of _onDispose() (async _currentEl drift).

Tests 73-74 cover nested markup (el > ul > li[each]) with both sync
and deferred (animate-leave) render paths. Test 72 updated for slot
property name.
fix(http): rework prepend scroll compensation mechanism
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.

1 participant