fix(web): drop the redundant second mirror

The first version coupled the layers from two places: a `scroll` listener
and a layout effect keyed on the committed draft. Mutation-testing each
hook alone against the built client shows the effect never fires the only
assignment that matters — with just the layout effect disabled the browser
scenario stays green, while disabling just the listener fails it.

Both premises behind the effect were wrong. Typing scrolls the caret into
view, which is an ordinary `scroll`. A draft that shrinks past the current
offset clamps both layers to the same maximum, because their extents are
equal — measured in chromium at 964/964, 1012/1012, 844/844 and 820/820 for
plain, soft-wrapped, unbreakable-run and highlighted drafts — and the
textarea's clamp fires `scroll` too.

The hazard the effect was imagined to cover does not exist either: React
replacing every child of the backdrop when the decoration set changes shape
preserves `scrollTop` (measured: 300 stays 300 through a full child
replacement), and the only replacement that zeroes it shrinks the content
below the offset, which is the clamp case already covered.

The e2e's edit case survives, retitled to say what it actually pins: that
typing is not a separate case needing its own mirror. The unit spec now
asserts the backdrop tracks a second move back to the top, which a one-shot
mirror would fail.
This commit is contained in:
creatixchu
2026-07-31 12:02:38 +08:00
parent a7b7066267
commit 07448093c5
6 changed files with 35 additions and 40 deletions

View File

@@ -230,12 +230,12 @@ describe('web e2e: composer draft scrolling', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('an edit re-mirrors the layers without a scroll event', async () => {
it('typing at the end of a scrolled draft keeps the layers together', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-draft-scroll-edit'))
// Typing at the caret — parked at the draft's end by the wheel gesture —
// reflows both layers. The textarea need not scroll (the caret stays in
// view), so nothing fires the `scroll` listener and only the draft-keyed
// mirror keeps the layers together.
// The other way the box moves. Typing at the caret — parked at the draft's
// end by the wheel gesture — scrolls it into view, which is a `scroll` like
// any other; this pins that an edit is not a separate case needing its own
// mirror, which is why one listener is the whole implementation.
const input = page.locator('textarea:enabled').first()
await input.press('End')
await input.pressSequentially(' tail')