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).
48 lines
2.3 KiB
TypeScript
48 lines
2.3 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 /* ... */ 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*:/)
|
||
})
|
||
})
|