Skip to content

Commit e1d47dd

Browse files
authored
Merge pull request #998 from mathuo/fix/github-issue-991-group-activation
bug: fix always renderers initial position
2 parents 65b68a6 + 4901434 commit e1d47dd

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,104 @@ describe('dockviewComponent', () => {
13631363

13641364
expect(state).toEqual(api.toJSON());
13651365
});
1366+
1367+
test('always visible renderer positioning after fromJSON', async () => {
1368+
dockview.layout(1000, 1000);
1369+
1370+
// Create a layout with both onlyWhenVisible and always visible panels
1371+
dockview.fromJSON({
1372+
activeGroup: 'group-1',
1373+
grid: {
1374+
root: {
1375+
type: 'branch',
1376+
data: [
1377+
{
1378+
type: 'leaf',
1379+
data: {
1380+
views: ['panel1', 'panel2'],
1381+
id: 'group-1',
1382+
activeView: 'panel1',
1383+
},
1384+
size: 500,
1385+
},
1386+
{
1387+
type: 'leaf',
1388+
data: {
1389+
views: ['panel3'],
1390+
id: 'group-2',
1391+
activeView: 'panel3',
1392+
},
1393+
size: 500,
1394+
},
1395+
],
1396+
size: 1000,
1397+
},
1398+
height: 1000,
1399+
width: 1000,
1400+
orientation: Orientation.HORIZONTAL,
1401+
},
1402+
panels: {
1403+
panel1: {
1404+
id: 'panel1',
1405+
contentComponent: 'default',
1406+
title: 'panel1',
1407+
renderer: 'onlyWhenVisible',
1408+
},
1409+
panel2: {
1410+
id: 'panel2',
1411+
contentComponent: 'default',
1412+
title: 'panel2',
1413+
renderer: 'always',
1414+
},
1415+
panel3: {
1416+
id: 'panel3',
1417+
contentComponent: 'default',
1418+
title: 'panel3',
1419+
renderer: 'always',
1420+
},
1421+
},
1422+
});
1423+
1424+
// Wait for next animation frame to ensure positioning is complete
1425+
await new Promise((resolve) => requestAnimationFrame(resolve));
1426+
1427+
const panel2 = dockview.getGroupPanel('panel2')!;
1428+
const panel3 = dockview.getGroupPanel('panel3')!;
1429+
1430+
// Verify that always visible panels have been positioned
1431+
const overlayContainer = dockview.overlayRenderContainer;
1432+
1433+
// Check that panels with renderer: 'always' are attached to overlay container
1434+
expect(panel2.api.renderer).toBe('always');
1435+
expect(panel3.api.renderer).toBe('always');
1436+
1437+
// Get the overlay elements for always visible panels
1438+
const panel2Overlay = overlayContainer.element.querySelector('[data-panel-id]') as HTMLElement;
1439+
const panel3Overlay = overlayContainer.element.querySelector('[data-panel-id]:not(:first-child)') as HTMLElement;
1440+
1441+
// Verify positioning has been applied (should not be 0 after layout)
1442+
if (panel2Overlay) {
1443+
const style = getComputedStyle(panel2Overlay);
1444+
expect(style.position).toBe('absolute');
1445+
expect(style.left).not.toBe('0px');
1446+
expect(style.top).not.toBe('0px');
1447+
expect(style.width).not.toBe('0px');
1448+
expect(style.height).not.toBe('0px');
1449+
}
1450+
1451+
// Test that updateAllPositions method works correctly
1452+
const updateSpy = jest.spyOn(overlayContainer, 'updateAllPositions');
1453+
1454+
// Call fromJSON again to trigger position updates
1455+
dockview.fromJSON(dockview.toJSON());
1456+
1457+
// Wait for the position update to be called
1458+
await new Promise((resolve) => requestAnimationFrame(resolve));
1459+
1460+
expect(updateSpy).toHaveBeenCalled();
1461+
1462+
updateSpy.mockRestore();
1463+
});
13661464
});
13671465

13681466
test('add panel', () => {

packages/dockview-core/src/__tests__/overlay/overlayRenderContainer.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,111 @@ describe('overlayRenderContainer', () => {
347347
expect(referenceContainer.element.getBoundingClientRect).toHaveBeenCalledTimes(2);
348348
expect(parentContainer.getBoundingClientRect).toHaveBeenCalledTimes(2);
349349
});
350+
351+
test('updateAllPositions forces position recalculation for visible panels', async () => {
352+
const cut = new OverlayRenderContainer(
353+
parentContainer,
354+
fromPartial<DockviewComponent>({})
355+
);
356+
357+
const panelContentEl1 = document.createElement('div');
358+
const panelContentEl2 = document.createElement('div');
359+
360+
const onDidVisibilityChange1 = new Emitter<any>();
361+
const onDidDimensionsChange1 = new Emitter<any>();
362+
const onDidLocationChange1 = new Emitter<any>();
363+
364+
const onDidVisibilityChange2 = new Emitter<any>();
365+
const onDidDimensionsChange2 = new Emitter<any>();
366+
const onDidLocationChange2 = new Emitter<any>();
367+
368+
const panel1 = fromPartial<IDockviewPanel>({
369+
api: {
370+
id: 'panel1',
371+
onDidVisibilityChange: onDidVisibilityChange1.event,
372+
onDidDimensionsChange: onDidDimensionsChange1.event,
373+
onDidLocationChange: onDidLocationChange1.event,
374+
isVisible: true,
375+
location: { type: 'grid' },
376+
},
377+
view: {
378+
content: {
379+
element: panelContentEl1,
380+
},
381+
},
382+
group: {
383+
api: {
384+
location: { type: 'grid' },
385+
},
386+
},
387+
});
388+
389+
const panel2 = fromPartial<IDockviewPanel>({
390+
api: {
391+
id: 'panel2',
392+
onDidVisibilityChange: onDidVisibilityChange2.event,
393+
onDidDimensionsChange: onDidDimensionsChange2.event,
394+
onDidLocationChange: onDidLocationChange2.event,
395+
isVisible: false, // This panel is not visible
396+
location: { type: 'grid' },
397+
},
398+
view: {
399+
content: {
400+
element: panelContentEl2,
401+
},
402+
},
403+
group: {
404+
api: {
405+
location: { type: 'grid' },
406+
},
407+
},
408+
});
409+
410+
// Mock getBoundingClientRect for consistent testing
411+
jest.spyOn(referenceContainer.element, 'getBoundingClientRect')
412+
.mockReturnValue(
413+
fromPartial<DOMRect>({
414+
left: 100,
415+
top: 200,
416+
width: 150,
417+
height: 250,
418+
})
419+
);
420+
421+
jest.spyOn(parentContainer, 'getBoundingClientRect').mockReturnValue(
422+
fromPartial<DOMRect>({
423+
left: 50,
424+
top: 100,
425+
width: 200,
426+
height: 300,
427+
})
428+
);
429+
430+
// Attach both panels
431+
const container1 = cut.attach({ panel: panel1, referenceContainer });
432+
const container2 = cut.attach({ panel: panel2, referenceContainer });
433+
434+
await exhaustMicrotaskQueue();
435+
await exhaustAnimationFrame();
436+
437+
// Clear previous calls to getBoundingClientRect
438+
jest.clearAllMocks();
439+
440+
// Call updateAllPositions
441+
cut.updateAllPositions();
442+
443+
// Should trigger resize for visible panels only
444+
await exhaustAnimationFrame();
445+
446+
// Verify that positioning was updated for visible panel
447+
expect(container1.style.left).toBe('50px');
448+
expect(container1.style.top).toBe('100px');
449+
expect(container1.style.width).toBe('150px');
450+
expect(container1.style.height).toBe('250px');
451+
452+
// Verify getBoundingClientRect was called for visible panel only
453+
// updateAllPositions should call the resize function which triggers getBoundingClientRect
454+
expect(referenceContainer.element.getBoundingClientRect).toHaveBeenCalled();
455+
expect(parentContainer.getBoundingClientRect).toHaveBeenCalled();
456+
});
350457
});

packages/dockview-core/src/dockview/dockviewComponent.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,11 @@ export class DockviewComponent
16211621

16221622
this.updateWatermark();
16231623

1624+
// Force position updates for always visible panels after DOM layout is complete
1625+
requestAnimationFrame(() => {
1626+
this.overlayRenderContainer.updateAllPositions();
1627+
});
1628+
16241629
this._onDidLayoutFromJSON.fire();
16251630
}
16261631

packages/dockview-core/src/overlay/overlayRenderContainer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class OverlayRenderContainer extends CompositeDisposable {
6161
disposable: IDisposable;
6262
destroy: IDisposable;
6363
element: HTMLElement;
64+
resize?: () => void;
6465
}
6566
> = {};
6667

@@ -85,6 +86,22 @@ export class OverlayRenderContainer extends CompositeDisposable {
8586
);
8687
}
8788

89+
updateAllPositions(): void {
90+
if (this._disposed) {
91+
return;
92+
}
93+
94+
// Invalidate position cache to force recalculation
95+
this.positionCache.invalidate();
96+
97+
// Call resize function directly for all visible panels
98+
for (const entry of Object.values(this.map)) {
99+
if (entry.panel.api.isVisible && entry.resize) {
100+
entry.resize();
101+
}
102+
}
103+
}
104+
88105
detatch(panel: IDockviewPanel): boolean {
89106
if (this.map[panel.api.id]) {
90107
const { disposable, destroy } = this.map[panel.api.id];
@@ -290,6 +307,8 @@ export class OverlayRenderContainer extends CompositeDisposable {
290307
this.map[panel.api.id].disposable.dispose();
291308
// and reset the disposable to the active reference-container
292309
this.map[panel.api.id].disposable = disposable;
310+
// store the resize function for direct access
311+
this.map[panel.api.id].resize = resize;
293312

294313
return focusContainer;
295314
}

0 commit comments

Comments
 (0)