Files
apes-sidebar/tests/panel-host-css.spec.ts
chendefine 96650fa189 fix(shell): keep an overflow:hidden fallback beneath clip + guard the cascade
shift fix) — but as a BARE declaration. Engines without clip support
(Safari < 16, older Chromium) drop an unsupported declaration at parse
time, which leaves the host `overflow: visible`: no clipping at all,
and the historical "collapsed panels extend the document's scrollable
area, whole page draggable" regression returns on those engines.

Declare `overflow: hidden` BEFORE `overflow: clip`: clip-aware engines
apply the later clip (identical behavior to #453), while old engines
fall back to hidden and keep clipping (with its pre-clip
scrollability — the same state they had before #453, strictly better
than visible). The cascade order is the contract.

Also extends the rule comment (the fix guards the whole
scroll-into-view class — focus moving into off-viewport regions,
nested iframes/workbenches claiming focus while loading — not just the
reveal case) and adds tests/panel-host-css.spec.ts guarding the
cascade (hidden first, clip last), the fixed viewport layer, and that
the degraded rule never reintroduces overflow. Contract documented in
the integration guide §12 (root anchor bullet).
2026-09-02 23:51:13 +08:00

48 lines
2.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 /* ... *​/ comment blocks 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*:/)
})
})