fix(client): keep fullscreen sidebar strips draggable via fine-grained dockkit subtraction

Drop the fullscreen panel's whole-box no-drag: it swallowed the strips'
blank-run window drag and made no-drag the steady state, turning the
open/close app-region nudge into a computed-value no-op (electron#32341)
exactly when a hidden panel opens straight into fullscreen. The panel now
declares no steady-state app-region in either presentation, and ui-dockkit
subtracts the one box base.css never matches — the plain-div split divider —
alongside tabs, strip-end chrome, and pane bodies. New specs pin both
contracts against the stylesheets on disk.
This commit is contained in:
Yif committed 2026-09-19 16:19:44 +08:00
1 parent c6c1df6869
commit 9e95a7b183
4 files changed
+100 -19

No files matched your search

@@ -223,15 +223,18 @@
/* macOS desktop: an embedder may pin this surface under the window's top drag
band (ui-layout .leadingBand — full-window geometry that Electron composes
in DOM order, not by stacking). The chip run, the strip-end chrome, and the
pane body subtract their whole boxes so tabs, controls, and content stay
clickable even where they overlap the band; the strip's blank fill and top
inset keep the band's drag, so the row moves the window like a native tab
bar. Hidden surfaces contribute nothing: Blink skips non-visible boxes when
it collects draggable regions. */
in DOM order, not by stacking). The chip run, the strip-end chrome, the
pane body, and the split divider subtract their whole boxes so tabs,
controls, content, and the resize grab stay usable even where they overlap
the band; the strip's blank fill and top inset keep the band's drag, so the
row moves the window like a native tab bar. The divider must be named here:
it is a plain div, so ui-web base.css's interactive-element subtraction
never matches it. Hidden surfaces contribute nothing: Blink skips
non-visible boxes when it collects draggable regions. */
:global(html[data-platform='darwin']) .stripTabs,
:global(html[data-platform='darwin']) .stripChrome,
:global(html[data-platform='darwin']) .paneBody {
:global(html[data-platform='darwin']) .paneBody,
:global(html[data-platform='darwin']) .divider {
-webkit-app-region: no-drag;
}
@@ -0,0 +1,28 @@
/**
* macOS drag-band subtraction, asserted against dockkit.module.css on disk:
* an embedder may pin the surface under the window's top drag band, and
* Electron composes app-regions from geometry in DOM order — every box that
* must stay usable subtracts itself. The divider must be listed explicitly:
* it is a plain div, so ui-web base.css's interactive-element subtraction
* never matches it, and an unsubtracted vertical divider's top run would
* drag the window instead of resizing the split.
*/
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const css = readFileSync(resolve(import.meta.dirname, '../src/components/dockkit.module.css'), 'utf8')
describe('macOS app-region subtraction', () => {
it('subtracts the chip run, strip-end chrome, pane body, and divider; never declares drag', () => {
const rules = [...css.matchAll(/(?<sel>[^{}]+)\{(?<body>[^{}]*)\}/g)]
.map(match => [match.groups!['sel']!.trim(), match.groups!['body']!] as const)
.filter(([, body]) => body.includes('-webkit-app-region'))
expect(rules).toHaveLength(1)
const [selector, body] = rules[0]!
expect(/-webkit-app-region:\s*([^;]+);/.exec(body)?.[1]?.trim()).toBe('no-drag')
for (const part of ['.stripTabs', '.stripChrome', '.paneBody', '.divider']) {
expect(selector, part).toContain(`:global(html[data-platform='darwin']) ${part}`)
}
})
})
@@ -49,9 +49,12 @@
/* One-frame app-region pulse at each open/close edge (RightbarSeat): changing
this computed value is what makes Electron recollect the window's drag
rects, which the slide's transform/visibility flips alone never dirty
(electron#32341). While set, the whole panel is out of the drag band for
that frame, which is also the desired end state everywhere but the strip's
blank fill. */
(electron#32341). The panel's steady state sets no app-region in either
presentation, so this write is a real change at both edges — including a
hidden panel opening straight into fullscreen (a narrow window's first
open), whose rects were skipped while hidden. While set, the whole panel is
out of the drag band for that frame; the removal frame recollects the
steady-state geometry below. */
:global(html[data-platform='darwin']) .panel[data-sidebar-right-region-nudge] {
-webkit-app-region: no-drag;
}
@@ -63,15 +66,16 @@
border: none;
}
/* Electron drag regions are window geometry, not stacking: the frame's leading
band and the sidebar logo row's drag rects underneath keep taking the pointer
through this fixed overlay, turning non-interactive panel content (file
preview text, images, webviews) into window drag. The panel appears after
both in DOM order, so this subtracts its whole box; the strips' blank runs
give up the drag affordance in exchange. */
:global(html[data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] {
-webkit-app-region: no-drag;
}
/* No whole-box subtraction here: Electron drag regions are window geometry,
not stacking, so the frame's leading band does keep acting through this
fixed overlay — but the kit already subtracts every box that must stay
usable (tabs, strip-end chrome, pane bodies, dividers; ui-dockkit), and
ui-web base.css subtracts interactive elements and portalled overlays.
What remains of the band under the strips' blank runs is the window drag
affordance, the same native-tab-bar reading as the push presentation. A
panel-wide no-drag would trade it away and, worse, make no-drag the
steady state, turning the open/close pulse above into a computed-value
no-op exactly when a hidden panel opens straight into fullscreen. */
/* Windows keeps caption controls above fullscreen file and terminal panels. */
:global([data-windows-titlebar]) .panel[data-sidebar-right-panel='fullscreen'] {
@@ -0,0 +1,46 @@
/**
* App-region nudge pulse, asserted against SidebarRight.module.css on disk:
* Electron recollects the window's drag rects only when a style pass changes
* a computed app-region value (electron#32341), so the panel's steady state
* must declare NO app-region in either presentation — the open/close no-drag
* pulse is then a real computed-value change at both edges, including a
* hidden panel opening straight into fullscreen (a narrow window's first
* open, whose rects were skipped while hidden). A steady-state value on the
* panel (the old fullscreen whole-box no-drag) makes the pulse a silent
* no-op and leaves the frame's drag band swallowing the strip's top-right
* controls; fine-grained subtraction belongs to ui-dockkit (tabs, strip-end
* chrome, pane bodies, dividers), leaving the strips' blank runs draggable.
*/
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const css = readFileSync(new URL('../src/client/shell/SidebarRight.module.css', import.meta.url), 'utf8')
/**
* Every `<selector> -> value` pair of rules declaring `-webkit-app-region`.
* @returns one entry per declaring rule, in source order.
*/
function appRegionRules(): [selector: string, value: string][] {
const rules: [string, string][] = []
for (const match of css.matchAll(/(?<sel>[^{}]+)\{(?<body>[^{}]*)\}/g)) {
const value = /-webkit-app-region:\s*([^;]+);/.exec(match.groups!['body']!)?.[1]
if (value !== undefined) rules.push([match.groups!['sel']!.trim(), value.trim()])
}
return rules
}
describe('app-region nudge pulse styles', () => {
it('pulses the panel to no-drag', () => {
const pulses = appRegionRules()
.filter(([selector]) => selector.includes('[data-sidebar-right-region-nudge]'))
expect(pulses).toHaveLength(1)
expect(pulses[0]![0]).toMatch(/\.panel\[data-sidebar-right-region-nudge\]\s*$/)
expect(pulses[0]![1]).toBe('no-drag')
})
it('declares no steady-state app-region, so the pulse is a real change', () => {
const steady = appRegionRules()
.filter(([selector]) => !selector.includes('[data-sidebar-right-region-nudge]'))
expect(steady).toEqual([])
})
})