fix(desktop): make WSLg window-control buttons clickable

The renderer-drawn min/max/close called event.preventDefault() on
pointerdown to keep WSLg from stealing keyboard focus on maximize, but
preventDefault on pointerdown suppresses the synthesized click under WSLg's
XWayland/RAIL compositor — the buttons rendered but never fired. Switch to
stopPropagation (the pattern the native titlebar tools already use), which
stops the drag region from swallowing the press while leaving the click
intact. Focus reassertion after maximize is already handled main-side in
performWindowControl via win.focus().
This commit is contained in:
Austin Pickett 2026-07-24 11:10:05 -04:00
parent cdc8d2e3d7
commit 73ce7f9bed
2 changed files with 15 additions and 6 deletions

View file

@ -72,7 +72,7 @@ describe('WslgWindowControls', () => {
expect(screen.queryByLabelText('Window controls')).toBeNull()
})
it('prevents pointer activation from stealing renderer focus', () => {
it('stops pointerdown propagation without cancelling the click', () => {
desktopWindow.hermesDesktop = { windowControls } as unknown as Window['hermesDesktop']
renderControls()
const event = new MouseEvent('pointerdown', { bubbles: true, cancelable: true })
@ -81,7 +81,10 @@ describe('WslgWindowControls', () => {
fireEvent(button, event)
fireEvent.click(button)
expect(event.defaultPrevented).toBe(true)
// preventDefault on pointerdown kills the synthesized click under WSLg's
// RAIL compositor, so the button must NOT cancel the default — only stop
// propagation so the drag region doesn't swallow the press.
expect(event.defaultPrevented).toBe(false)
expect(windowControls.toggleMaximize).toHaveBeenCalledOnce()
})

View file

@ -21,7 +21,13 @@ interface WslgWindowControlsProps {
const buttonClass =
'grid h-full w-[46px] place-items-center border-0 bg-transparent p-0 text-muted-foreground transition-colors duration-75 select-none [-webkit-app-region:no-drag] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring hover:bg-white/10 hover:text-foreground active:bg-white/15'
const preserveRendererFocus = (event: PointerEvent<HTMLButtonElement>) => event.preventDefault()
// Match the native titlebar tools: stopPropagation (NOT preventDefault) on
// pointerdown. preventDefault on pointerdown suppresses the synthesized click
// under WSLg's XWayland/RAIL compositor, so the buttons render but never fire.
// stopPropagation keeps the drag region from swallowing the press while leaving
// the click intact; keyboard-focus reassertion after maximize is handled on the
// main side in performWindowControl (win.focus()).
const stopTitlebarDrag = (event: PointerEvent<HTMLButtonElement>) => event.stopPropagation()
export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowControlsProps) {
const location = useLocation()
@ -43,7 +49,7 @@ export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowCont
aria-label="Minimize window"
className={buttonClass}
onClick={controls.minimize}
onPointerDown={preserveRendererFocus}
onPointerDown={stopTitlebarDrag}
type="button"
>
<Codicon name="chrome-minimize" size={10} />
@ -52,7 +58,7 @@ export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowCont
aria-label={isMaximized ? 'Restore window' : 'Maximize window'}
className={buttonClass}
onClick={controls.toggleMaximize}
onPointerDown={preserveRendererFocus}
onPointerDown={stopTitlebarDrag}
type="button"
>
<Codicon name={isMaximized ? 'chrome-restore' : 'chrome-maximize'} size={10} />
@ -64,7 +70,7 @@ export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowCont
'hover:bg-[#c42b1c] hover:text-white active:bg-[#b3271a] active:text-white dark:hover:bg-[#c42b1c]'
)}
onClick={controls.close}
onPointerDown={preserveRendererFocus}
onPointerDown={stopTitlebarDrag}
type="button"
>
<Codicon name="chrome-close" size={10} />