From 5220404676f1b4b04efa8acccd9ab128c0c683e2 Mon Sep 17 00:00:00 2001 From: Cody Beebe Date: Thu, 16 Jul 2026 08:54:10 -0400 Subject: [PATCH 1/2] fix(core): resolve sash/scrollbar pointer listeners from the element's document Splitview registers a sash's pointerdown on the sash element, but registers the follow-up pointermove/pointerup/pointercancel/contextmenu listeners on the global document. An element inside a popout window dispatches its pointer events into that window's document, so those listeners never fire: the drag arms on pointerdown and then nothing moves, and the sash cannot be dragged. Scrollbar repeats the pattern, so the tab-bar overflow scrollbar is affected too. Floating groups render into the main document, which is why the global document happens to be correct for them and only popout groups break. Resolve the owning document from the element instead. It is captured once per drag into a const closed over by both the add and the matching remove, so a listener can never be added on one document and removed from another. --- packages/dockview-core/src/scrollbar.ts | 14 ++++++++------ .../dockview-core/src/splitview/splitview.ts | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/dockview-core/src/scrollbar.ts b/packages/dockview-core/src/scrollbar.ts index cbeedf03b..ea2854cd7 100644 --- a/packages/dockview-core/src/scrollbar.ts +++ b/packages/dockview-core/src/scrollbar.ts @@ -89,17 +89,19 @@ export class Scrollbar extends CompositeDisposable { this.calculateScrollbarStyles(); }; + const doc = this.element.ownerDocument; + const onEnd = () => { toggleClass(this.element, 'dv-scrollable-scrolling', false); - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', onEnd); - document.removeEventListener('pointercancel', onEnd); + doc.removeEventListener('pointermove', onPointerMove); + doc.removeEventListener('pointerup', onEnd); + doc.removeEventListener('pointercancel', onEnd); }; - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onEnd); - document.addEventListener('pointercancel', onEnd); + doc.addEventListener('pointermove', onPointerMove); + doc.addEventListener('pointerup', onEnd); + doc.addEventListener('pointercancel', onEnd); }), addDisposableListener(this.element, 'scroll', () => { this.calculateScrollbarStyles(); diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index ab3343360..441858f5d 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -543,6 +543,8 @@ export class Splitview { this.layoutViews(); }; + const doc = sash.ownerDocument; + const end = () => { for (const item of this.viewItems) { item.enabled = true; @@ -552,18 +554,18 @@ export class Splitview { this.saveProportions(); - document.removeEventListener('pointermove', onPointerMove); - document.removeEventListener('pointerup', end); - document.removeEventListener('pointercancel', end); - document.removeEventListener('contextmenu', end); + doc.removeEventListener('pointermove', onPointerMove); + doc.removeEventListener('pointerup', end); + doc.removeEventListener('pointercancel', end); + doc.removeEventListener('contextmenu', end); this._onDidSashEnd.fire(undefined); }; - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', end); - document.addEventListener('pointercancel', end); - document.addEventListener('contextmenu', end); + doc.addEventListener('pointermove', onPointerMove); + doc.addEventListener('pointerup', end); + doc.addEventListener('pointercancel', end); + doc.addEventListener('contextmenu', end); }; sash.addEventListener('pointerdown', onPointerStart); From 5feed0ab612e2e489b8bb9e9eff19c60d98fe771 Mon Sep 17 00:00:00 2001 From: Cody Beebe Date: Thu, 16 Jul 2026 09:20:45 -0400 Subject: [PATCH 2/2] test(core): cover sash dragging in a non-global document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives a sash drag on a Splitview built inside a second document (document.implementation.createHTMLDocument), which is the shape a popout window has: the element lives in that window's document, so its pointer events are dispatched there. Fails before the ownerDocument fix — the pointermove never reaches the listener bound to the global document, so the views never resize. The existing sash-drag test dispatches on the global document, where sash.ownerDocument === document, so it passes either way and cannot catch this. --- .../src/__tests__/splitview/splitview.spec.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index a7ddfab71..adf4fc6fd 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -677,6 +677,61 @@ describe('splitview', () => { expect(removeEventListenerSpy).toHaveBeenCalledTimes(4); }); + test("dnd: sash drag follows pointer events in the element's own document", () => { + // A popout window (api.addPopoutGroup) has its own document, and the + // group's element is moved into it. The sash's pointer events are then + // dispatched into THAT document — so listeners bound to the global + // document never fire and the sash cannot be dragged. + const otherDocument = document.implementation.createHTMLDocument(); + const otherContainer = otherDocument.createElement('div'); + otherDocument.body.appendChild(otherContainer); + + const splitview = new Splitview(otherContainer, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(400, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + + const sashElement = otherContainer + .getElementsByClassName('dv-sash') + .item(0) as HTMLElement; + + expect(sashElement).toBeTruthy(); + expect([view1.size, view2.size]).toEqual([200, 200]); + + fireEvent( + sashElement, + new MouseEvent('pointerdown', { clientX: 50, clientY: 100 }) + ); + + // expect a delta move of 70 - 50 = 20, driven from the owning document + fireEvent( + otherDocument, + new MouseEvent('pointermove', { clientX: 70, clientY: 110 }) + ); + expect([view1.size, view2.size]).toEqual([220, 180]); + + fireEvent( + otherDocument, + new MouseEvent('pointerup', { clientX: 70, clientY: 110 }) + ); + + // the drag has ended, so further moves must not resize + fireEvent( + otherDocument, + new MouseEvent('pointermove', { clientX: 100, clientY: 110 }) + ); + expect([view1.size, view2.size]).toEqual([220, 180]); + + splitview.dispose(); + }); + test('should restore iframe pointer events on contextmenu during sash drag', () => { const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); const removeEventListenerSpy = jest.spyOn(