fix: keep Tabs scroll extent in sync after tab switches and async refreshes (OK-57257 OK-57977)#12530
fix: keep Tabs scroll extent in sync after tab switches and async refreshes (OK-57257 OK-57977)#12530huhuanming wants to merge 3 commits into
Conversation
aef493d to
10223a7
Compare
232f949 to
84c4b39
Compare
84c4b39 to
ab8ee16
Compare
ab8ee16 to
c477277
Compare
| (candidate, index) => candidate === observedElementsRef.current[index], | ||
| ); | ||
| // Same measurement source and signal elements -> nothing to re-attach. | ||
| if (element && isObservingSameElements) { |
There was a problem hiding this comment.
当 Tabs.ScrollView 初始没有子节点时,Container 会先通过 fallback 将 ScrollView 根节点作为唯一的 ResizeObserver target。随后 ScrollView 正式注册同一个根节点时,新旧 target 列表仍都是 [root],这里会直接返回,但此时 MutationObserver 还没有创建。
之后异步插入内容只会增加被 flex 布局固定的根节点 scrollHeight,根节点 border-box 不变,ResizeObserver 不会触发,共享列表高度仍会停留在旧值。
请让已注册的 .onekey-tabs-scroll-view 只有在对应 MutationObserver 已存在时才走这个快捷返回;否则继续完成 observer 初始化。
There was a problem hiding this comment.
已修复(fcda326d1f)。observer 复用条件现在会同时确认 ResizeObserver 已存在;对于已注册的 .onekey-tabs-scroll-view,还必须确认 mutationObserverRef.current 已建立。这样 fallback [root] 切换到 registered [root] 时,即使 targets 相同也会继续完成 MutationObserver 初始化。
| ]), | ||
| ); | ||
| const lockedHeight = scrollViewRoot.getBoundingClientRect().height; | ||
| scrollViewRoot.style.setProperty('height', `${lockedHeight}px`); |
There was a problem hiding this comment.
probe 注入后,如果后续的滚轮操作、getMetrics() 或截图抛错,当前恢复代码不会执行,ScrollView 会一直保留被锁定的 height、min-height、max-height、flex 和 probe 节点。这样后续重跑会在已污染的在线页面上采样,可能持续误报。
请将注入后的验证流程放入 try/finally,并在 finally 中无条件删除 probe、恢复原始内联样式和清理 dataset。
There was a problem hiding this comment.
已修复(fcda326d1f)。已抽出幂等的 cleanupGrowthProbe(),注入前先清理可能的旧残留,并将 probe 注入、滚轮验证、getMetrics()、截图及结果生成整体放入 try/finally;finally 会无条件删除 probe、恢复原始内联样式并清理 dataset。语法、Oxlint、场景加载和 diff 检查均已通过。
| } | ||
| apply(element); | ||
| }); | ||
| mo.observe(registeredElement, { childList: true, subtree: true }); |
There was a problem hiding this comment.
🟠 P1 mo.observe 传入可空的 registeredElement,tsc 编译失败
问题
isRegisteredScrollView 是布尔值(registeredElement?.classList.contains('onekey-tabs-scroll-view') ?? false),无法对 registeredElement 做类型收窄。因此在 if (isRegisteredScrollView && ...) 块内,registeredElement 仍是 HTMLElement | null,而 MutationObserver.observe(target: Node) 不接受 null。
mo.observe(registeredElement, { childList: true, subtree: true });
// Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Node'.影响
yarn lint 的 TypeScript 检查在 Container.tsx:501 直接失败(CI lint (24.x) 已 fail),阻塞合并。这是上一处修复把内联 registeredElement?.classList... 守卫替换为 isRegisteredScrollView 布尔后丢失收窄而引入的回归。
建议
在守卫中保留对 registeredElement 的真值判断以恢复非空收窄(getObservedElements 已用 isRegisteredScrollView && registeredElement 做过同样处理,这里保持一致):
if (
isRegisteredScrollView &&
registeredElement &&
typeof MutationObserver !== 'undefined'
) {
...
mo.observe(registeredElement, { childList: true, subtree: true });
}Generated by Claude Code
OK-57257 OK-57977
Summary
Tabs.ContainerTabs.ScrollViewTabs.Listpages on their existing explicit-height contractReported problems
OK-57257 — DeFi cannot reach the bottom after switching tabs
The issue video starts on a tall DeFi page, switches to the shorter NFT tab, and then returns to DeFi. Wheel scrolling stops at an older, shorter extent, leaving the lower protocol sections and Support Hub cards unreachable.
The layout transition is tall -> short -> tall. Restoring the DeFi scene can increase its live
scrollHeightwithout rebuilding the shared horizontal Tabs list at the same height.OK-57977 — the active DeFi page cannot reach the bottom after an account/network change
The Jira video shows the same symptom without requiring a tab change:
This is a different trigger from OK-57257, but the same shared layout failure: the focused tab receives taller async content while the outer Tabs list retains an earlier height.
QA retest — relaunch while DeFi is active
QA reported an additional stable path after testing the first version of this PR:
The attached logs show the Home UI initializing first, followed several seconds later by the supported-protocol and position responses under the configured slow-4G profile. This makes the first DeFi measurement an initial/skeleton measurement; later data changes the DOM while DeFi remains focused.
Why the first shared fix was still incomplete
The first revision correctly moved the workaround out of
DeFiContainerand made the registeredTabs.ScrollViewroot the live measurement source. It also attachedResizeObserverto the root and its direct content nodes.However, those direct nodes were captured only once when the observer was attached. On relaunch or another async refresh, React can add or replace content after that snapshot:
ResizeObservertargetTabs.ScrollViewborder box can remain pinned by its parent layoutscrollHeightchanges, which is not itself aResizeObservernotificationThis explains QA's sequence precisely: switching tabs after an update re-attached the observer and temporarily fixed the page; relaunching into a fresh async DeFi load recreated the stale snapshot.
Root cause and relationship between the issues
All three paths increase a focused tab's live content extent while the shared horizontal list still owns an older numeric height. The failure belongs to
Tabs.Container, not DeFi margin/padding and not a background-service scroll state.Fix
For registered
Tabs.ScrollViewpages,Tabs.Containernow combines two observers with separate responsibilities:ResizeObserverwatches the root and its current direct content nodes for intrinsic size changes such as image, font, and layout resolution.MutationObserverwatches structuralchildListchanges in the focused ScrollView subtree.scrollHeightand writes that live value to the shared horizontal list.The mutation observer intentionally excludes attribute/style observation, so updating the list height cannot feed back into the observer. The fix is generic to every flow-content tab using
Tabs.ScrollView; business pages do not need selectors, focus-specific timers, or height reports.The previous DeFi-specific registration is removed. In particular, it no longer caches a sampled DeFi height that can override later live DOM measurements.
Stable reproduction script
Run against the desktop client on CDP port 9222:
The scenario now validates three phases:
The CDP connection also waits for the OneKey renderer to finish booting, so the scenario can be started together with the app instead of racing the initial page load.
Before/after evidence
Before this revision
On the first shared-fix implementation, adding a
160 pxdirect child produced:5178 pxscrollHeight:5338 px5178 px160 pxThe new node was absent from the one-time
ResizeObservertarget snapshot.After this revision
With the deterministic
96 pxprobe:5178 -> 5274 px5043 -> 5139 px5139 / 51390 pxThe structural observer detects the insertion, refreshes the target set, and measures the live root extent.
App relaunch validation
Validation used the real release-based Electron client with desktop network throttling enabled (
slow4g,562.5 mslatency).After a full process exit and restart (
isSoftRestart: false), the scenario waited for DeFi's async content, then checked the bottom before any tab round trip:scrollTop = maxScrollTop = 50435178 / 5178 px0 px0 pxcoldStartClipped=falseThe subsequent DeFi -> NFT -> DeFi check and direct-node growth probe also passed. A separate full run repeated the tab round trip 8 times; all 8 were clean.
QA acceptance
Please recheck all three routes with wheel input:
Repeat long -> short -> long data transitions where possible. None of the routes should require a recovery tab switch.
Checks
release/v6.5.0at2ebdd41fddc4772776fe) and 4 changed filesnode --check scenarios/regression.mjsoxlint --deny-warnings: 0 warnings, 0 errorsoxlint --deny-warnings: 0 warnings, 0 errorstsgo -p ./tsconfig.json --noEmit: passedgit diff --check: passedREGRESSION PASSyarn agent:check --profile commitcannot spawn its child commands correctly on this Windows checkout (exitCode: nullin 0-3 ms).yarn lint:stagedalso relies on Unix-onlygrep/xargs. The corresponding oxlint, syntax, diff, and tsgo checks above were run directly and passed.