diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index f4f1cbfb2..70cfc0fd3 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -421,6 +421,153 @@ describe('dockviewComponent', () => { expect(query.length).toBe(3); }); + test('that moving a popout group to specific position works correctly', async () => { + window.open = () => setupMockWindow(); + + dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + }); + + dockview.layout(600, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + position: { direction: 'right' }, + }); + + await dockview.addPopoutGroup(panel1.api.group); + expect(panel1.api.location.type).toBe('popout'); + expect(dockview.groups.length).toBe(3); // panel2 + hidden reference + popout + + // Move popout group to left of panel2 + panel1.api.group.api.moveTo({ + group: panel2.api.group, + position: 'left', + }); + + // Core assertions: should be back in grid and positioned correctly + expect(panel1.api.location.type).toBe('grid'); + expect(dockview.groups.length).toBe(2); // Should clean up properly + expect(dockview.panels.length).toBe(2); + + // Verify both panels are visible and accessible + expect(panel1.api.isVisible).toBe(true); + expect(panel2.api.isVisible).toBe(true); + }); + + test('that moving a popout group to different positions works', async () => { + window.open = () => setupMockWindow(); + + dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + }); + + dockview.layout(600, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + position: { direction: 'right' }, + }); + + await dockview.addPopoutGroup(panel1.api.group); + expect(panel1.api.location.type).toBe('popout'); + + // Test moving to different positions + ['top', 'bottom', 'left', 'right'].forEach(position => { + panel1.api.group.api.moveTo({ + group: panel2.api.group, + position: position as any, + }); + + // Should be back in grid and work correctly regardless of position + expect(panel1.api.location.type).toBe('grid'); + expect(panel1.api.isVisible).toBe(true); + expect(panel2.api.isVisible).toBe(true); + expect(dockview.groups.length).toBeGreaterThanOrEqual(2); + expect(dockview.panels.length).toBe(2); + }); + }); + + test('that reference group cleanup works when moving popout to new position', async () => { + window.open = () => setupMockWindow(); + + dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + }); + + dockview.layout(600, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + position: { direction: 'right' }, + }); + + // Store reference group ID before popout + const originalGroupId = panel1.group.id; + + await dockview.addPopoutGroup(panel1.api.group); + expect(panel1.api.location.type).toBe('popout'); + expect(dockview.groups.length).toBe(3); // panel2 + hidden reference + popout + + // Move to new position - should clean up reference group + panel1.api.group.api.moveTo({ + group: panel2.api.group, + position: 'right', + }); + + expect(panel1.api.location.type).toBe('grid'); + expect(dockview.groups.length).toBe(2); // Just panel2 + panel1 in new position + + // Reference group should be cleaned up (no longer exist) + const referenceGroupStillExists = dockview.groups.some(g => g.id === originalGroupId); + expect(referenceGroupStillExists).toBe(false); + }); + test('horizontal', () => { dockview = new DockviewComponent(container, { createComponent(options) { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 9151f55ff..ad69c25a1 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -2367,17 +2367,51 @@ export class DockviewComponent if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); } - selectedPopoutGroup.disposable.dispose(); + + // For cross-window moves, we need to prevent the automatic restoration + // to the reference group that happens in the disposal cleanup + const isMovingToNewTarget = to.api.location.type === 'grid'; + + if (isMovingToNewTarget) { + // For cross-window moves to new positions, we need to clean up properly + // Remove from popout groups list to prevent automatic restoration + const index = this._popoutGroups.indexOf(selectedPopoutGroup); + if (index >= 0) { + this._popoutGroups.splice(index, 1); + } + + // Clean up the reference group (ghost) if it exists and is hidden + if (selectedPopoutGroup.referenceGroup) { + const referenceGroup = this.getPanel(selectedPopoutGroup.referenceGroup); + if (referenceGroup && !referenceGroup.api.isVisible) { + this.doRemoveGroup(referenceGroup, { skipActive: true }); + } + } + + // Manually dispose the window without triggering restoration + selectedPopoutGroup.window.dispose(); + + // Update group's location and containers for main window + from.model.renderContainer = this.overlayRenderContainer; + from.model.dropTargetContainer = this.rootDropTargetContainer; + from.model.location = { type: 'grid' }; + } else { + // Normal disposal that will restore to reference group + selectedPopoutGroup.disposable.dispose(); + } } } - if (from.api.location.type !== 'popout') { - const referenceLocation = getGridLocation(to.element); - const dropLocation = getRelativeLocation( - this.gridview.orientation, - referenceLocation, - target - ); + // For moves to grid locations (including cross-window moves from popout) + const referenceLocation = getGridLocation(to.element); + const dropLocation = getRelativeLocation( + this.gridview.orientation, + referenceLocation, + target + ); + + // Add to grid for moves from non-popout sources, or for cross-window moves from popout + if (from.api.location.type !== 'popout' || to.api.location.type === 'grid') { let size: number;