fix(autoscroll): re-anchor on composer resize #147
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/mention-autocomplete-breaks-autoscroll"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reported: after using an
@mention(autocomplete pick or hand-typed token), the room/DM autoscroll appears to stop working. New messages arrive in the database and over WS but the view does not move to them. Same symptom for any input that grows the composer textarea past one line.Root cause: the autoscroll hook only inspects scroll geometry on htmx swap events. When the composer textarea grows (mention insert calls
ta.dispatchEvent(new Event('input'))which runs theoninputhandler and bumpsta.style.height), the#messagesflex child shrinks from the bottom.scrollTopis unchanged by the browser, soscrollTop + clientHeightis now belowscrollHeight - slackandisAtBottom()returns false. The next message swap fireshtmx:beforeSwap, snapshotswasAtBottom = false, and thehtmx:afterSwaprestore is a no-op. The user perceives autoscroll as broken even though they never scrolled.Fix: add a
ResizeObserveron#messagesinpartials/auto_scroll.html. On every container resize, if the pre-resizewasAtBottomflag is still true (no realscrollevent has fired to flip it), re-anchor to the bottom. Teardown disconnects the observer alongside the existing event-listener cleanup so the WS reconnect soft-refresh does not leak observers.ResizeObserveris feature-detected for safety; all supported browsers have it.The partial is included by both
room/page.htmlanddm/page.html, so the fix lands in both surfaces at once. No backend changes;just checkclean.Manual repro:
@+ a letter, pick a user from the popover (textarea grows by one line).Reported: after using an @mention (autocomplete pick or hand-typed token), the room/DM autoscroll appears to stop working - new messages arrive but the view does not move to them. Same symptom for any input that grows the composer textarea past one line. Root cause: the autoscroll hook only inspects scroll geometry on htmx swap events. When the composer textarea grows (mention insert calls `ta.dispatchEvent(new Event('input'))` which runs the `oninput` handler and bumps `ta.style.height`), the `#messages` flex child shrinks from the bottom. `scrollTop` is unchanged, so `scrollTop + clientHeight` now sits below `scrollHeight - slack` and `isAtBottom()` returns false. The next message swap fires `htmx:beforeSwap`, snapshots `wasAtBottom = false`, and the `htmx:afterSwap` restore is a no-op. The user perceives autoscroll as broken even though they never scrolled. Fix: add a `ResizeObserver` on `#messages` in `partials/auto_scroll.html`. On every container resize, if the pre-resize `wasAtBottom` flag is still true (no real `scroll` event has fired to flip it), re-anchor to the bottom. Teardown disconnects the observer alongside the existing event-listener cleanup so the WS reconnect soft-refresh does not leak observers. `ResizeObserver` is feature-detected for safety; all supported browsers have it. The partial is included by both `room/page.html` and `dm/page.html`, so the fix lands in both surfaces at once.wasAtBottomfrom scroll events onlyReal root cause: `htmx:beforeCleanupElement` BUBBLES. The previous teardown was attached to the auto_scroll script's parent (`<main>` via the `data-lc-cleanup-root` fallback) with `{ once: true }`. The mention popover refresh swaps `innerHTML` on `#lc-mention-popover`, which makes htmx clean up the previous `<ul>`. The `beforeCleanupElement` event for that cleaned `<ul>` bubbles up through the composer form, the layout wrappers, and into `<main>`, where it fires the autoscroll teardown ONCE. After that, the scroll listener, both htmx swap listeners, and the ResizeObserver are gone. From the user's perspective, autoscroll appears to start working, then dies after the first or second mention-related popover interaction. Fix: attach the teardown listener to `#messages` itself and filter on `evt.target === messages`. Descendant cleanups (message edits via OOB outerHTML, etc.) still bubble through but the filter drops them. Real teardown still fires when `#messages` itself is detached as part of a page swap (the cleanup walk fires `beforeCleanupElement` on every removed element, including `#messages`). Remove the `{ once: true }` flag and gate on an explicit `torndown` boolean so a descendant bubble cannot consume the listener before the real teardown event reaches it. This is a class of bug: any other IIFE in the codebase that uses the same `root.addEventListener('htmx:beforeCleanupElement', teardown, { once: true })` pattern has the same problem and will tear itself down on the first descendant cleanup. Out of scope for this PR; the composer and room/page WS-subscribe scripts both use this pattern and should be revisited.