fix(markdown): preview TOC dismiss-on-outside-click, popover z-index, code-block banner stickiness

Three preview (render) bugs where DSH's chat-optimized markdown primitives
leak into the file-preview scroll world:

- CodeBlock banner: DSH's bannerWrap is position:sticky (top:0, z-index:6) so a
  block keeps its copy action reachable inside a per-message scroll container.
  In the preview every fence shares one .editorMd scroll container, so the
  banner pins to the viewport top until the whole block scrolls past and reads
  as a floating header. Neutralize stickiness for non-mermaid fences in the
  preview (mermaid chrome replaces the banner and is excluded via
  data-mermaid-processed).

- TOC popover z-index: .tocBar was z-index:3, below the code banner's 6, so the
  outline panel painted beneath a sticky fence header. Raise the bar to 7.

- TOC dismissal: only the toggle re-click closed the popover. Add a document
  pointerdown listener that closes on an outside press (blank preview space),
  exempting the button and panel so the toggle still flips.

Adds md-toc unit coverage for outside-click dismiss and toggle-preserving
behavior; records the deviations in the TOC design doc.
This commit is contained in:
Guochao
2026-09-03 09:56:31 +08:00
committed by Menghuan1918
parent 396017b459
commit a2a26901e6
4 changed files with 83 additions and 3 deletions
+18 -1
View File
@@ -68,13 +68,30 @@ export function MdToc(): ReactNode {
// Esc closes the popover (the panel is a sibling of the button, so focus
// stays wherever the user came from — a document listener is the simplest).
// A document pointerdown also closes it when the click lands outside the
// button and the panel, so clicking blank preview space dismisses the
// outline instead of requiring a second click on the toggle. The check runs
// at pointerdown (before the button's own click toggles), and the button and
// panel are exempt so a re-click on the toggle keeps working as a toggle
// rather than being eaten by the dismiss.
useLayoutEffect(() => {
if (!open) return
const onKey = (event: KeyboardEvent): void => {
if (event.key === 'Escape') setOpen(false)
}
const onPointerDown = (event: Event): void => {
const target = event.target
if (!(target instanceof Node)) return
const bar = barRef.current
if (bar !== null && bar.contains(target)) return
setOpen(false)
}
document.addEventListener('keydown', onKey)
return () => { document.removeEventListener('keydown', onKey) }
document.addEventListener('pointerdown', onPointerDown)
return () => {
document.removeEventListener('keydown', onKey)
document.removeEventListener('pointerdown', onPointerDown)
}
}, [open])
const jump = (entry: TocEntry): void => {