bug: fix always renderers initial position

This commit is contained in:
mathuo 2025-08-22 21:55:44 +01:00
parent e9df48e294
commit 49014345d9
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
4 changed files with 229 additions and 0 deletions

View File

@ -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', () => {

View File

@ -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();
});
});

View File

@ -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();
}

View File

@ -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;
}