Files
apes-sidebar/tests/panel-host-css.spec.ts
T
Menghuan1918 b07c7116c8 fix(desktop): 补齐 mermaid 放大视图的拖拽区退出 + 把 #772 的守卫换成真实级联探针
#773 修掉了面板宿主层继承宿主 `body > :not(#root) { no-drag }` 导致的 macOS 拖拽
失效,但只覆盖了 `.mermaidModal` 之外的层——而放大视图是插件**第二个铺满视口的
body 直挂层**(`position:fixed; inset:0`,portal 到 body),打开期间会同样清空整窗
拖拽与 macOS 双击标题栏缩放。

- src/client/sidebar.module.css:`.mermaidModal` 加 `-webkit-app-region: initial
  !important`(与面板宿主同一条契约),并在面板宿主那段注释里写明「哪些交互层
  绝对不能加 initial」(`.selectionPopup` 是 `<button>`、FloatingWindow /
  AnchoredPopover 自带指针拖拽,宿主 `:is(button, …)` 已给它们 no-drag;反过来
  声明 `initial !important` 会让按下变成拖窗,重演 #103/#111)。
- tests/panel-host-css.spec.ts:新用例把形状钉在 Linux 的 `pnpm test` 里(宿主层
  与面板层必须是 `initial !important`、`> *` 必须是 `no-drag`、`.mermaidModal`
  同样退出、`.selectionPopup` 不得出现 `initial`)——没有 macOS runner,丢掉
  `!important` 只能靠这里才不会静默复发。
- tests/e2e/mount.e2e.ts:把 #773 的**文本正则**断言换成**真实级联探针**——逐字
  注入宿主两条 darwin 规则、翻转 `data-platform=darwin`,读取计算值并断言
  `[data-window-drag]` 仍 `drag`、宿主层与面板层为 `none`、`[data-dsh-panel-host]
  > *` 为 `no-drag`,随后还原。探针把 `[data-window-drag]` 放在 `#root` 内:直挂
  body 的元素会被宿主那条 id 级规则本身命中(这正是问题机制)。
- docs/external-plugin-guide.md §12.1:按仓库规则同步这条皮肤契约(含交互弹层
  的反例判据与守卫位置)。

实测(scratch profile + 真实 dsh web + Chromium):
- npm dsh-better-sidebar@0.22.0:探针红,`host` 计算值 `no-drag`(正是线上症状)
- 本分支打包版:探针绿(`strip=drag` / `host=none` / `layer=none` / `child=no-drag`)
- 本地挂载 lane:7 passed(mount + perf,含新探针)

验证:typecheck / lint 通过;panel-host-css + theme 用例通过;新产物
`app-region:initial!important` 2 处(面板宿主 + mermaid 模态)、`app-region:no-drag` 2 处。
2026-09-28 11:04:52 +08:00

74 lines
3.8 KiB
TypeScript

/**
* The unified panel host layer must clip WITHOUT being a scroll container —
* the guard for the "panels / toggle cluster jumped away from the viewport
* corner" bug class.
*
* `overflow: hidden` only clips: the box stays a scroll container, so any
* script scroll or the browser's native scroll-into-view fixup (focus moving
* into an off-viewport region, a nested workbench/iframe claiming focus
* while it loads, focus() landing during a panel's slide-out transition)
* walks up to the nearest scrollable ancestor — this layer — and scrolls it,
* dragging every panel plus the toggle cluster off the corner while the
* computed `left`/`right` still read correct (the offset hides in the box's
* own scroll offset). `overflow: clip` declared AFTER `hidden` keeps the
* `hidden` fallback for engines without clip support and removes the
* scrollability everywhere else; the cascade order is part of the contract.
*/
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const css = readFileSync('src/client/sidebar.module.css', 'utf8')
/** Body of the `:global([data-dsh-panel-host])` rule (up to the un-indented `}`). */
const hostRule = css.match(/:global\(\[data-dsh-panel-host\]\) \{([\s\S]*?)\n\}/)?.[1]
/** Strip block comments (`/*` to `*`-`/`) so prose mentions don't count as declarations. */
const stripComments = (cssText: string): string => cssText.replace(/\/\*[\s\S]*?\*\//g, '')
describe('panel host layer css', () => {
it('declares the clip cascade: hidden first, clip last', () => {
expect(hostRule, 'the [data-dsh-panel-host] rule must exist').toBeDefined()
const declarations = stripComments(hostRule!).match(/overflow\s*:\s*\w+/g) ?? []
expect(declarations).toEqual(['overflow: hidden', 'overflow: clip'])
})
it('stays the fixed viewport layer (containing block + BFC guarantee)', () => {
expect(hostRule).toMatch(/position:\s*fixed/)
expect(hostRule).toMatch(/inset:\s*0/)
})
it('does not reintroduce overflow on the degraded (absolute) layer', () => {
const degraded = css.match(
/:global\(\[data-dsh-panel-host\]\[data-dsh-panel-host-degraded\]\) \{([\s\S]*?)\n\}/,
)?.[1]
expect(degraded, 'the degraded-mode rule must exist').toBeDefined()
expect(degraded).not.toMatch(/overflow\s*:/)
})
it('keeps the app-region opt-out that un-breaks macOS window drag (issue #772)', () => {
// The shell's `html[data-platform=darwin] body > :not(#root) { no-drag }`
// carries an id in its specificity, so only `initial !important` wins;
// `initial` computes to the NEUTRAL `none` (does not subtract from the
// drag region) while `no-drag` is the subtracting value — hence the
// split: decorative viewport-sized layers opt out, their panels stay
// no-drag so controls keep receiving clicks. Dropping the `!important`
// silently re-breaks window drag / double-click-title zoom on macOS
// (there is no macOS runner to catch it), so pin the shape here where
// Linux `pnpm test` fails instead.
const text = stripComments(css)
expect(text, 'the panel host must opt out of app-region computation').toMatch(
/:global\(\[data-dsh-panel-host\]\)\s*\{[^}]*-webkit-app-region:\s*initial\s*!important/,
)
expect(text, 'panels must stay no-drag').toMatch(
/:global\(\[data-dsh-panel-host\]\)\s*>\s*\*\s*\{[^}]*-webkit-app-region:\s*no-drag/,
)
expect(text, 'the mermaid zoom modal is the other viewport-sized body child').toMatch(
/\.mermaidModal\s*\{[^}]*-webkit-app-region:\s*initial\s*!important/,
)
// The interactive popups must NOT get `initial`: they are buttons/handles
// the shell already keeps no-drag, and outranking that rule makes a press
// start a window drag (the #103/#111 click-swallow class).
expect(text).not.toMatch(/\.selectionPopup\s*\{[^}]*-webkit-app-region:\s*initial/)
})
})