diff --git a/src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts b/src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts index 3b55c60a1a1..6aeb2c60f81 100644 --- a/src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts +++ b/src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts @@ -76,7 +76,13 @@ describe('webview registry drag listeners', () => { registerPersistentWebview('page-1', createWebview()) - expect(addedListeners.map((entry) => entry.type)).toEqual(['dragstart', 'dragend', 'drop']) + expect(addedListeners.map((entry) => entry.type)).toEqual([ + 'dragstart', + 'dragend', + 'drop', + 'pointerdown', + 'pointermove' + ]) }) it('removes drag listeners after the last webview is destroyed', async () => { @@ -86,7 +92,7 @@ describe('webview registry drag listeners', () => { registerPersistentWebview('page-1', createWebview()) registerPersistentWebview('page-2', createWebview()) - expect(addedListeners).toHaveLength(3) + expect(addedListeners).toHaveLength(5) destroyPersistentWebview('page-1') @@ -94,7 +100,13 @@ describe('webview registry drag listeners', () => { destroyPersistentWebview('page-2') - expect(removedListeners.map((entry) => entry.type)).toEqual(['dragstart', 'dragend', 'drop']) + expect(removedListeners.map((entry) => entry.type)).toEqual([ + 'dragstart', + 'dragend', + 'drop', + 'pointerdown', + 'pointermove' + ]) expect(unregisterGuestMock).toHaveBeenCalledWith({ browserPageId: 'page-1' }) expect(unregisterGuestMock).toHaveBeenCalledWith({ browserPageId: 'page-2' }) }) @@ -124,13 +136,35 @@ describe('webview registry drag listeners', () => { expect(secondWebview.style.pointerEvents).toBe('auto') }) + it('releases native drag passthrough when a cancelled dragstart never starts a drag', async () => { + const { registerPersistentWebview } = await import('./webview-registry') + const webview = createWebview() + webview.style.pointerEvents = 'auto' + registerPersistentWebview('page-1', webview) + const listener = (type: string): ((event: Event) => void) => { + const found = addedListeners.find((entry) => entry.type === type)?.listener + if (typeof found !== 'function') { + throw new Error(`${type} listener missing`) + } + return found + } + + for (const pointerType of ['pointermove', 'pointerdown']) { + listener('dragstart')(new Event('dragstart')) + expect(webview.style.pointerEvents).toBe('none') + // No dragend or drop follows a cancelled dragstart; the next pointer event proves no drag is live. + listener(pointerType)(new Event(pointerType)) + expect(webview.style.pointerEvents).toBe('auto') + } + }) + it('keeps one listener set across repeated registrations', async () => { const { registerPersistentWebview } = await import('./webview-registry') registerPersistentWebview('page-1', createWebview()) registerPersistentWebview('page-2', createWebview()) - expect(addedListeners).toHaveLength(3) + expect(addedListeners).toHaveLength(5) }) it('profiles live webviews and registered browser guests for memory breadcrumbs', async () => { diff --git a/src/renderer/src/components/browser-pane/host-guest/webview-registry.ts b/src/renderer/src/components/browser-pane/host-guest/webview-registry.ts index 5d40d57cc08..0ecd4c475aa 100644 --- a/src/renderer/src/components/browser-pane/host-guest/webview-registry.ts +++ b/src/renderer/src/components/browser-pane/host-guest/webview-registry.ts @@ -44,6 +44,8 @@ type DragListenerRegistry = { dragstart: () => void dragend: () => void drop: () => void + pointerdown: () => void + pointermove: () => void } function getListenerHost(): (Window & { [DRAG_LISTENER_KEY]?: DragListenerRegistry }) | null { @@ -62,6 +64,8 @@ function removeDragListeners(): void { window.removeEventListener('dragstart', existingListeners.dragstart, true) window.removeEventListener('dragend', existingListeners.dragend, true) window.removeEventListener('drop', existingListeners.drop, true) + window.removeEventListener('pointerdown', existingListeners.pointerdown, true) + window.removeEventListener('pointermove', existingListeners.pointermove, true) delete listenerHost[DRAG_LISTENER_KEY] dragListenersAttached = false nativeDragPassthroughRelease?.() @@ -81,13 +85,19 @@ function ensureDragListeners(): void { const dragstart = (): void => setWebviewsDragPassthrough(true) const dragend = (): void => setWebviewsDragPassthrough(false) const drop = (): void => setWebviewsDragPassthrough(false) + // Why: a cancelled dragstart never starts a drag, so no dragend or drop follows and every webview + // would stay click-through. A live native drag delivers no pointer events, so one proves it is over. + const pointerdown = (): void => setWebviewsDragPassthrough(false) + const pointermove = (): void => setWebviewsDragPassthrough(false) window.addEventListener('dragstart', dragstart, true) window.addEventListener('dragend', dragend, true) window.addEventListener('drop', drop, true) + window.addEventListener('pointerdown', pointerdown, true) + window.addEventListener('pointermove', pointermove, true) // Why: only live webviews need drag passthrough listeners; removing them // when the registry empties keeps browserless sessions free of global hooks. - listenerHost[DRAG_LISTENER_KEY] = { dragstart, dragend, drop } + listenerHost[DRAG_LISTENER_KEY] = { dragstart, dragend, drop, pointerdown, pointermove } dragListenersAttached = true }