fix: scope the "Show in folder" reveal scroll to the tree body

A reveal on a deep tree shifted the whole panel (tab bar included) out
of the viewport: the reveal used native scrollIntoView, which scrolls
every scrollable ancestor, and the fixed panel host
([data-dsh-panel-host], overflow hidden) is still a programmatically
scrollable container — the host scrolled instead of (or besides) the
tree, with no way to scroll back. Reported on DSH 0.1.2-alpha.1.

Two layers:
- FileTree: compute the centered, clamped target and scrollTo the tree
  body only; scrollIntoView is never called.
- panel host: overflow hidden -> clip. A hidden box remains
  programmatically scrollable; clip keeps the same viewport-edge
  clipping (collapsed panels sliding off-screen) while making the host
  impossible to scroll, killing the whole leak class (md-toc jumps,
  future consumer scrollIntoView calls).
This commit is contained in:
Menghuan1918
2026-08-29 01:52:55 +08:00
parent 50e05ecea0
commit 052edb7685
3 changed files with 138 additions and 4 deletions
+13 -2
View File
@@ -280,10 +280,21 @@ export function FileTree(props: {
// (revealPaths), but the row may not be scrolled into sight — a reveal on
// a long tree should surface the highlighted file. Re-runs when the tree
// data or reveal set changes (the row appears after its level loads).
// Scrolls ONLY this tree's body: scrollIntoView would scroll every
// scrollable ancestor, and the clipping panel host
// ([data-dsh-panel-host]) is still programmatically scrollable — a deep
// reveal shifted the whole panel, tab bar included, out of the viewport.
useEffect(() => {
if (revealed.length === 0) return
const row = bodyRef.current?.querySelector('[data-dsh-revealed]')
row?.scrollIntoView({ block: 'center', behavior: 'smooth' })
const body = bodyRef.current
if (body === null) return
const row = body.querySelector<HTMLElement>('[data-dsh-revealed]')
if (row === null) return
const bodyTop = body.getBoundingClientRect().top
const rowRect = row.getBoundingClientRect()
const target = body.scrollTop + (rowRect.top + rowRect.height / 2) - (bodyTop + body.clientHeight / 2)
const max = Math.max(body.scrollHeight - body.clientHeight, 0)
body.scrollTo({ top: Math.min(Math.max(target, 0), max), behavior: 'smooth' })
}, [revealed, data])
/** Copy `text`; on success flip the row's copied label for a moment. */
+6 -2
View File
@@ -44,8 +44,12 @@
page draggable left-right and up-down while any panel was collapsed.
The host is exactly viewport-sized (inset: 0), so the clip is invisible
until a panel slides in from the edge; internal scroll containers and
the app's fixed overlay stack are unaffected. */
overflow: hidden;
the app's fixed overlay stack are unaffected. `clip`, not `hidden`: a
hidden box stays a PROGRAMMATICALLY scrollable container, and a
scrollIntoView fired from panel content (the file tree's "Show in
folder" reveal) scrolled the host itself — shifting the whole panel,
tab bar included, out of the viewport with no way to scroll back. */
overflow: clip;
}
:global([data-dsh-panel-host][data-dsh-panel-host-degraded]) {