fix(chat): widen composer re-pin observer to #composerWrap (Fable #5516 finding)
Fable senior review (COMMENT, no blockers) + DRY nit: - Widen the ResizeObserver from #msg to the whole #composerWrap so growth from the attachment tray / selection chips (not just textarea rows) also re-pins a bottom- pinned reader. Fable flagged tray/chip growth as a likely REMAINING #5515 trigger — same viewport-shrink physics, now covered by the same guarded re-pin at one seam. #composerWrap contains #msg, so the textarea path is still covered. - DRY: _repinMessagesAfterComposerResize() now reuses _messageBottomDistance() instead of re-implementing the bottom-distance expression inline. Re-verified live (composer grow -> bottomDist stays 0) + 524 scroll/composer regression tests green + 6 issue tests green.
This commit is contained in:
parent
dde008748e
commit
7e7e71f695
3 changed files with 22 additions and 20 deletions
|
|
@ -1918,27 +1918,25 @@ $('msg').addEventListener('input',()=>{
|
|||
});
|
||||
// #5514/#5515: re-pin the transcript on ANY composer height change, not only the
|
||||
// ones that route through the input->autoResize path. A multi-line paste
|
||||
// (WisprFlow), a draft restore, a programmatic value set, or a font/reflow can
|
||||
// all grow the composer and shrink the flex:1 transcript viewport, stranding a
|
||||
// pinned reader above the bottom (reads as a "random" upward jump — #5515). A
|
||||
// ResizeObserver on the textarea catches every case at one seam. The re-pin
|
||||
// itself is guarded (only fires when genuinely pinned), so this never fights a
|
||||
// reader who scrolled away. First callback fires on observe (initial size) — the
|
||||
// guard makes that a cheap no-op.
|
||||
// (WisprFlow), a draft restore, an attachment tray / selection-chip appearing, a
|
||||
// programmatic value set, or a font/reflow can all grow the composer and shrink
|
||||
// the flex:1 transcript viewport, stranding a pinned reader above the bottom
|
||||
// (reads as a "random" upward jump — #5515). Observe the whole #composerWrap
|
||||
// (not just #msg) so tray/chip growth is covered too, at one seam. The re-pin is
|
||||
// guarded (only fires when genuinely pinned), so it never fights a reader who
|
||||
// scrolled away. First callback fires on observe (initial size) — the guard
|
||||
// makes that a cheap no-op.
|
||||
(()=>{
|
||||
const _c=$('msg');
|
||||
if(!_c || typeof ResizeObserver!=='function' || typeof _repinMessagesAfterComposerResize!=='function') return;
|
||||
let _lastComposerH=_c.offsetHeight;
|
||||
const _cw=$('composerWrap')||$('msg');
|
||||
if(!_cw || typeof ResizeObserver!=='function' || typeof _repinMessagesAfterComposerResize!=='function') return;
|
||||
let _lastComposerH=_cw.offsetHeight;
|
||||
const _ro=new ResizeObserver(()=>{
|
||||
const h=_c.offsetHeight;
|
||||
if(h===_lastComposerH) return; // width-only / no-op resize: ignore
|
||||
const grew=h>_lastComposerH;
|
||||
const h=_cw.offsetHeight;
|
||||
if(h<=_lastComposerH){_lastComposerH=h;return;} // shrink/no-op: enlarges the viewport, can't strand
|
||||
_lastComposerH=h;
|
||||
// Only a GROW shrinks the transcript viewport; a shrink enlarges it and can't
|
||||
// strand a pinned reader. Re-pin on grow.
|
||||
if(grew) _repinMessagesAfterComposerResize();
|
||||
_repinMessagesAfterComposerResize(); // grow: re-pin the pinned reader
|
||||
});
|
||||
try{ _ro.observe(_c); }catch(_){ }
|
||||
try{ _ro.observe(_cw); }catch(_){ }
|
||||
})();
|
||||
// Track IME composition for East Asian input. Safari fires the committing
|
||||
// keydown AFTER compositionend with isComposing=false, so we also keep a
|
||||
|
|
|
|||
|
|
@ -5395,7 +5395,7 @@ function _repinMessagesAfterComposerResize(){
|
|||
if(!el) return;
|
||||
// Already at/very near the bottom? nothing to do (avoids needless writes while
|
||||
// idle-reading a short conversation that isn't scrollable).
|
||||
if(el.scrollHeight-el.scrollTop-el.clientHeight<=1) return;
|
||||
if(_messageBottomDistance()<=1) return;
|
||||
if(typeof _setMessageScrollToBottom==='function') _setMessageScrollToBottom();
|
||||
else { el.scrollTop=el.scrollHeight; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,11 +70,14 @@ def test_autoresize_calls_the_repin():
|
|||
|
||||
|
||||
def test_resize_observer_installed_on_composer():
|
||||
# A ResizeObserver on #msg re-pins on grow (catches non-autoResize paths).
|
||||
# A ResizeObserver on the composer wrapper re-pins on grow (catches
|
||||
# tray/chip/paste height changes, not just the typed-input seam).
|
||||
assert "new ResizeObserver(" in BOOT_JS
|
||||
assert "_repinMessagesAfterComposerResize" in BOOT_JS
|
||||
# Observes the whole wrapper so attach-tray / selection-chip growth is covered.
|
||||
assert "$('composerWrap')" in BOOT_JS
|
||||
# Only re-pin on GROW (a shrink enlarges the viewport, can't strand a reader).
|
||||
assert "if(grew) _repinMessagesAfterComposerResize();" in BOOT_JS
|
||||
assert "can't strand" in BOOT_JS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -94,6 +97,7 @@ def _run(scenario):
|
|||
// messages pane: scrollHeight fixed; clientHeight shrinks when composer grows.
|
||||
const el = { scrollHeight: 8768, clientHeight: 745, scrollTop: %(scrolltop)s };
|
||||
const $ = (id) => (id === 'messages' ? el : null);
|
||||
function _messageBottomDistance(){ return el.scrollHeight - el.scrollTop - el.clientHeight; }
|
||||
function _setMessageScrollToBottom(){ el.scrollTop = el.scrollHeight - el.clientHeight; el._pinnedCalled = true; }
|
||||
|
||||
%(helper)s
|
||||
|
|
|
|||
Loading…
Reference in a new issue