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:
@@ -72,7 +72,7 @@ describe('WslgWindowControls', () => {
|
|||||||
expect(screen.queryByLabelText('Window controls')).toBeNull()
|
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']
|
desktopWindow.hermesDesktop = { windowControls } as unknown as Window['hermesDesktop']
|
||||||
renderControls()
|
renderControls()
|
||||||
const event = new MouseEvent('pointerdown', { bubbles: true, cancelable: true })
|
const event = new MouseEvent('pointerdown', { bubbles: true, cancelable: true })
|
||||||
@@ -81,7 +81,10 @@ describe('WslgWindowControls', () => {
|
|||||||
fireEvent(button, event)
|
fireEvent(button, event)
|
||||||
fireEvent.click(button)
|
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()
|
expect(windowControls.toggleMaximize).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ interface WslgWindowControlsProps {
|
|||||||
const buttonClass =
|
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'
|
'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) {
|
export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowControlsProps) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -43,7 +49,7 @@ export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowCont
|
|||||||
aria-label="Minimize window"
|
aria-label="Minimize window"
|
||||||
className={buttonClass}
|
className={buttonClass}
|
||||||
onClick={controls.minimize}
|
onClick={controls.minimize}
|
||||||
onPointerDown={preserveRendererFocus}
|
onPointerDown={stopTitlebarDrag}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Codicon name="chrome-minimize" size={10} />
|
<Codicon name="chrome-minimize" size={10} />
|
||||||
@@ -52,7 +58,7 @@ export function WslgWindowControls({ isFullscreen, isMaximized }: WslgWindowCont
|
|||||||
aria-label={isMaximized ? 'Restore window' : 'Maximize window'}
|
aria-label={isMaximized ? 'Restore window' : 'Maximize window'}
|
||||||
className={buttonClass}
|
className={buttonClass}
|
||||||
onClick={controls.toggleMaximize}
|
onClick={controls.toggleMaximize}
|
||||||
onPointerDown={preserveRendererFocus}
|
onPointerDown={stopTitlebarDrag}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Codicon name={isMaximized ? 'chrome-restore' : 'chrome-maximize'} size={10} />
|
<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]'
|
'hover:bg-[#c42b1c] hover:text-white active:bg-[#b3271a] active:text-white dark:hover:bg-[#c42b1c]'
|
||||||
)}
|
)}
|
||||||
onClick={controls.close}
|
onClick={controls.close}
|
||||||
onPointerDown={preserveRendererFocus}
|
onPointerDown={stopTitlebarDrag}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Codicon name="chrome-close" size={10} />
|
<Codicon name="chrome-close" size={10} />
|
||||||
|
|||||||
Reference in New Issue
Block a user