diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 455299720..1c76d5433 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -1363,6 +1363,104 @@ describe('dockviewComponent', () => { expect(state).toEqual(api.toJSON()); }); + + test('always visible renderer positioning after fromJSON', async () => { + dockview.layout(1000, 1000); + + // Create a layout with both onlyWhenVisible and always visible panels + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1', 'panel2'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, + }, + { + type: 'leaf', + data: { + views: ['panel3'], + id: 'group-2', + activeView: 'panel3', + }, + size: 500, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + title: 'panel1', + renderer: 'onlyWhenVisible', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + renderer: 'always', + }, + panel3: { + id: 'panel3', + contentComponent: 'default', + title: 'panel3', + renderer: 'always', + }, + }, + }); + + // Wait for next animation frame to ensure positioning is complete + await new Promise((resolve) => requestAnimationFrame(resolve)); + + const panel2 = dockview.getGroupPanel('panel2')!; + const panel3 = dockview.getGroupPanel('panel3')!; + + // Verify that always visible panels have been positioned + const overlayContainer = dockview.overlayRenderContainer; + + // Check that panels with renderer: 'always' are attached to overlay container + expect(panel2.api.renderer).toBe('always'); + expect(panel3.api.renderer).toBe('always'); + + // Get the overlay elements for always visible panels + const panel2Overlay = overlayContainer.element.querySelector('[data-panel-id]') as HTMLElement; + const panel3Overlay = overlayContainer.element.querySelector('[data-panel-id]:not(:first-child)') as HTMLElement; + + // Verify positioning has been applied (should not be 0 after layout) + if (panel2Overlay) { + const style = getComputedStyle(panel2Overlay); + expect(style.position).toBe('absolute'); + expect(style.left).not.toBe('0px'); + expect(style.top).not.toBe('0px'); + expect(style.width).not.toBe('0px'); + expect(style.height).not.toBe('0px'); + } + + // Test that updateAllPositions method works correctly + const updateSpy = jest.spyOn(overlayContainer, 'updateAllPositions'); + + // Call fromJSON again to trigger position updates + dockview.fromJSON(dockview.toJSON()); + + // Wait for the position update to be called + await new Promise((resolve) => requestAnimationFrame(resolve)); + + expect(updateSpy).toHaveBeenCalled(); + + updateSpy.mockRestore(); + }); }); test('add panel', () => { diff --git a/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts b/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts index 274c9225a..6efb6bf33 100644 --- a/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts @@ -347,4 +347,111 @@ describe('overlayRenderContainer', () => { expect(referenceContainer.element.getBoundingClientRect).toHaveBeenCalledTimes(2); expect(parentContainer.getBoundingClientRect).toHaveBeenCalledTimes(2); }); + + test('updateAllPositions forces position recalculation for visible panels', async () => { + const cut = new OverlayRenderContainer( + parentContainer, + fromPartial({}) + ); + + const panelContentEl1 = document.createElement('div'); + const panelContentEl2 = document.createElement('div'); + + const onDidVisibilityChange1 = new Emitter(); + const onDidDimensionsChange1 = new Emitter(); + const onDidLocationChange1 = new Emitter(); + + const onDidVisibilityChange2 = new Emitter(); + const onDidDimensionsChange2 = new Emitter(); + const onDidLocationChange2 = new Emitter(); + + const panel1 = fromPartial({ + api: { + id: 'panel1', + onDidVisibilityChange: onDidVisibilityChange1.event, + onDidDimensionsChange: onDidDimensionsChange1.event, + onDidLocationChange: onDidLocationChange1.event, + isVisible: true, + location: { type: 'grid' }, + }, + view: { + content: { + element: panelContentEl1, + }, + }, + group: { + api: { + location: { type: 'grid' }, + }, + }, + }); + + const panel2 = fromPartial({ + api: { + id: 'panel2', + onDidVisibilityChange: onDidVisibilityChange2.event, + onDidDimensionsChange: onDidDimensionsChange2.event, + onDidLocationChange: onDidLocationChange2.event, + isVisible: false, // This panel is not visible + location: { type: 'grid' }, + }, + view: { + content: { + element: panelContentEl2, + }, + }, + group: { + api: { + location: { type: 'grid' }, + }, + }, + }); + + // Mock getBoundingClientRect for consistent testing + jest.spyOn(referenceContainer.element, 'getBoundingClientRect') + .mockReturnValue( + fromPartial({ + left: 100, + top: 200, + width: 150, + height: 250, + }) + ); + + jest.spyOn(parentContainer, 'getBoundingClientRect').mockReturnValue( + fromPartial({ + left: 50, + top: 100, + width: 200, + height: 300, + }) + ); + + // Attach both panels + const container1 = cut.attach({ panel: panel1, referenceContainer }); + const container2 = cut.attach({ panel: panel2, referenceContainer }); + + await exhaustMicrotaskQueue(); + await exhaustAnimationFrame(); + + // Clear previous calls to getBoundingClientRect + jest.clearAllMocks(); + + // Call updateAllPositions + cut.updateAllPositions(); + + // Should trigger resize for visible panels only + await exhaustAnimationFrame(); + + // Verify that positioning was updated for visible panel + expect(container1.style.left).toBe('50px'); + expect(container1.style.top).toBe('100px'); + expect(container1.style.width).toBe('150px'); + expect(container1.style.height).toBe('250px'); + + // Verify getBoundingClientRect was called for visible panel only + // updateAllPositions should call the resize function which triggers getBoundingClientRect + expect(referenceContainer.element.getBoundingClientRect).toHaveBeenCalled(); + expect(parentContainer.getBoundingClientRect).toHaveBeenCalled(); + }); }); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index c61a92c42..5171ffb55 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -1621,6 +1621,11 @@ export class DockviewComponent this.updateWatermark(); + // Force position updates for always visible panels after DOM layout is complete + requestAnimationFrame(() => { + this.overlayRenderContainer.updateAllPositions(); + }); + this._onDidLayoutFromJSON.fire(); } diff --git a/packages/dockview-core/src/overlay/overlayRenderContainer.ts b/packages/dockview-core/src/overlay/overlayRenderContainer.ts index 36217b47f..f51fb613d 100644 --- a/packages/dockview-core/src/overlay/overlayRenderContainer.ts +++ b/packages/dockview-core/src/overlay/overlayRenderContainer.ts @@ -61,6 +61,7 @@ export class OverlayRenderContainer extends CompositeDisposable { disposable: IDisposable; destroy: IDisposable; element: HTMLElement; + resize?: () => void; } > = {}; @@ -85,6 +86,22 @@ export class OverlayRenderContainer extends CompositeDisposable { ); } + updateAllPositions(): void { + if (this._disposed) { + return; + } + + // Invalidate position cache to force recalculation + this.positionCache.invalidate(); + + // Call resize function directly for all visible panels + for (const entry of Object.values(this.map)) { + if (entry.panel.api.isVisible && entry.resize) { + entry.resize(); + } + } + } + detatch(panel: IDockviewPanel): boolean { if (this.map[panel.api.id]) { const { disposable, destroy } = this.map[panel.api.id]; @@ -290,6 +307,8 @@ export class OverlayRenderContainer extends CompositeDisposable { this.map[panel.api.id].disposable.dispose(); // and reset the disposable to the active reference-container this.map[panel.api.id].disposable = disposable; + // store the resize function for direct access + this.map[panel.api.id].resize = resize; return focusContainer; }