Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DockviewComponent>({})
);

const panelContentEl1 = document.createElement('div');
const panelContentEl2 = document.createElement('div');

const onDidVisibilityChange1 = new Emitter<any>();
const onDidDimensionsChange1 = new Emitter<any>();
const onDidLocationChange1 = new Emitter<any>();

const onDidVisibilityChange2 = new Emitter<any>();
const onDidDimensionsChange2 = new Emitter<any>();
const onDidLocationChange2 = new Emitter<any>();

const panel1 = fromPartial<IDockviewPanel>({
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<IDockviewPanel>({
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<DOMRect>({
left: 100,
top: 200,
width: 150,
height: 250,
})
);

jest.spyOn(parentContainer, 'getBoundingClientRect').mockReturnValue(
fromPartial<DOMRect>({
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();
});
});
5 changes: 5 additions & 0 deletions packages/dockview-core/src/dockview/dockviewComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
19 changes: 19 additions & 0 deletions packages/dockview-core/src/overlay/overlayRenderContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class OverlayRenderContainer extends CompositeDisposable {
disposable: IDisposable;
destroy: IDisposable;
element: HTMLElement;
resize?: () => void;
}
> = {};

Expand All @@ -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];
Expand Down Expand Up @@ -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;
}
Expand Down