fix(frontend): stop reduced-motion transcript guards from compounding / 修复关闭窗口动画后会话滚动大跳 - #9753
Merged
SivanCola merged 2 commits intoSep 3, 2026
Conversation
Windows reports prefers-reduced-motion: reduce whenever "Animate controls and elements inside windows" is off. The global reduced-motion reset turned every transition into a 0.01ms `transition: all`, which still starts from the old value: transform and padding writes were invisible to same-frame getBoundingClientRect/scrollHeight reads. The reader-extent guard then re-measured its own unapplied transform as fresh displacement and compounded the visual offset on every scroll event (7088 → 14177 → 21265 px in the reported diagnostics), ending in a correction to scrollTop 0 and degraded question-jump navigation. - Reduced-motion CSS now removes transitions (`transition: none`) instead of shortening them, so geometry lands synchronously. Animations keep the 0.01ms squash so fill-mode: forwards end states survive. - The reader-extent guard and the anchor-compensation loop derive the physical drift from the item list's applied computed transform rather than the remembered offset, so a lagging or externally cleared guard can no longer compound. - Race tests cover the unapplied and applied transform cases; both fail against the previous hook. - Bundle budgets step one decimal: 462.7 → 462.9 KiB gzip (462.827 measured), 2469.4 → 2469.9 KiB raw (2469.815 measured).
… reset Problem: repolint rejected the PR head because the two new visual-guard scenarios pushed transcript-reader-extent-race.test.tsx to 853 lines, over the 800-line test-file ceiling. The CSS half of the fix also had no automated guard: nothing stopped a later edit from restoring `transition-duration: 0.01ms` and silently reintroducing the lagging same-frame geometry reads. Fix: - Move the unapplied/applied visual-guard scenarios into transcript-reader-visual-guard-race.test.tsx with the same JSDOM + fake rAF harness, restore the extent race file to its base contents, and add the new file to test:transcript and the motion CI contract. - Extend check-motion-ci-contract.mjs so the global universal prefers-reduced-motion reset must use `transition: none !important` and must not shorten transitions. Verification: - transcript-reader-visual-guard-race: 9 pass on the PR head; 2 fail against the pre-fix hook (-2043px guard, 1154px correction). - check-motion-ci-contract passes on the PR head and fails when the reset is reverted to transition-duration. - go run ./tools/repolint clean; pnpm test:transcript, test:motion, test:typecheck, lint:hooks pass; git diff --check clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
User report: on Win10 LTSC 1809 (8th-gen i7, GTX 1060) with all Windows visual effects disabled, the transcript still jumps by thousands of pixels while scrolling and the question-jump slider is unusable. Enabling "Animate controls and elements inside windows" makes both symptoms disappear.
That setting is what WebView2 maps to
prefers-reduced-motion: reduce(the attached diagnostics manifest hasreducedMotion: true). Our global reduced-motion reset turned every transition into a 0.01mstransition: all. A near-zero transition still starts from the old value, sotransform/paddingwrites are not visible to same-framegetBoundingClientRect/scrollHeightreads; they land one frame later. Verified in headless Chromium: normal mode reads the new value synchronously, the 0.01ms rule reads the old value until the next frame,transition: nonereads the new value synchronously again.The transcript scroll guards measure geometry right after their own writes, so:
reverseDisplacement7088.5 → 14177 → 21265.5 → 28354 → 35442.5 (exact multiples), followed by a correction toscrollTop 0. That is the reported jump.scrollHeightoscillated 9057 ↔ 16369 during the history prepend and the question-jump intent endeddegraded. That is the "slider unusable" report.Changes
styles.css: the reduced-motion block usestransition: none !importantinstead oftransition-duration: 0.01ms. Animations keep the 0.01ms squash sofill-mode: forwardsend states survive.transcriptReaderExtentStability.ts: newtranscriptAppliedVisualOffset()reads the item list's applied computed transform (matrix/matrix3dtranslateY) and falls back to the remembered offset only when nothing can be read.useTranscriptReaderExtentStability.ts(observe + tick correction) andtranscriptAnchorCompensation.tssubtract the applied offset rather than the remembered one, so a lagging or externally cleared guard cannot compound.-2043pxguard,1154pxcorrection) and pass now (-681px,681px).d8840832f: the two visual-guard race scenarios live intranscript-reader-visual-guard-race.test.tsx(the extent race file was back over the 800-line repolint ceiling), andcheck-motion-ci-contract.mjsnow asserts the global reduced-motion reset usestransition: none !importantand never shortens transitions, so the CSS half of the fix cannot regress silently.Verification
pnpm test(251 suites),typecheck,test:typecheck,lint:hooks,check:css,test:motion,pnpm buildall pass.bench/transcript-scroll-stability.mjswithreducedMotion: "reduce"emulation: 94/94 pass, including the reduced-motion tail and return-to-bottom gates.bench/transcript-reader-transaction.mjs: Chromium 20/20 rounds pass; the WebKit leg did not run locally (WebKit not installed).transcript-reader-visual-guard-race: 9 pass on the head; 2 fail against the pre-fix hook. The motion contract check fails when the reset is reverted totransition-duration.go run ./tools/repolintclean.Merge order
Merge this PR before #9754. Both touch
useTranscriptReaderExtentStability.ts(adjacent hunks inobserve()) andcheck-bundle-budget.mjs; a dry-run merge in either order conflicts on those two files, so #9754 should rebase ontomain-v2after this lands. The changes are semantically independent: this PR replaces the rememberedvisualOffsetwith the applied transform when derivingphysicalAnchorDrift; #9754 widens therejectedpredicate two lines below.Documentation-impact: none - behavior change is a bug fix in the scroll guards and a reduced-motion CSS reset; no documented user-facing behavior changes.