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
+40
View File
@@ -149,4 +149,44 @@ describe('MdToc', () => {
expect(container.querySelector('[data-dsh-md-toc-panel]')).toBeNull()
await unmount(root)
})
it('closes the popover when a pointerdown lands outside the button and panel', async () => {
const { container, root } = await mountToc([
{ tag: 'h1', text: 'Title' },
{ tag: 'h2', text: 'A' },
{ tag: 'h2', text: 'B' },
])
const button = container.querySelector<HTMLButtonElement>('[data-dsh-md-toc]')!
await act(async () => { button.click() })
expect(container.querySelector('[data-dsh-md-toc-panel]')).not.toBeNull()
// A press on a heading that sits in the scroll container (a sibling of the
// TOC bar) is "blank space" to the outline: it must dismiss the popover.
const heading = container.querySelector('h2')!
await act(async () => {
heading.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
})
expect(container.querySelector('[data-dsh-md-toc-panel]'), 'outside press must close the panel').toBeNull()
await unmount(root)
})
it('keeps the toggle working: a press inside the bar does not dismiss, a click re-closes', async () => {
const { container, root } = await mountToc([
{ tag: 'h1', text: 'Title' },
{ tag: 'h2', text: 'A' },
{ tag: 'h2', text: 'B' },
])
const button = container.querySelector<HTMLButtonElement>('[data-dsh-md-toc]')!
await act(async () => { button.click() })
expect(container.querySelector('[data-dsh-md-toc-panel]')).not.toBeNull()
// A pointerdown on the toggle itself must not be swallowed by the dismiss
// handler (the button re-enables pointer events inside the pointer-events:
// none bar), so the subsequent click still toggles it closed.
await act(async () => {
button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
})
expect(container.querySelector('[data-dsh-md-toc-panel]'), 'press on the toggle must not dismiss').not.toBeNull()
await act(async () => { button.click() })
expect(container.querySelector('[data-dsh-md-toc-panel]'), 're-clicking the toggle must close').toBeNull()
await unmount(root)
})
})