Skip to content

Commit 5cb5501

Browse files
committed
fix(golden-layout): support nested intersection drag handles
Replaced the one-level-deep loop with a small recursive walk
1 parent bf5092d commit 5cb5501

3 files changed

Lines changed: 367 additions & 226 deletions

File tree

packages/golden-layout/scss/goldenlayout-base.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ $height6: 15px; // Appears 1 time
8484
cursor: ew-resize;
8585
}
8686
}
87+
88+
// While part of an active 2D intersection (hover or drag), lift the line above
89+
// pane content so an offset T/cross junction renders cleanly instead of being
90+
// clipped by the neighbouring pane. Stays below the intersection handle
91+
// (z-index 60) so the handle always wins pointer priority for 2D dragging.
92+
&.lm_intersection_line {
93+
z-index: 59;
94+
}
8795
}
8896

8997
// Intersection splitter: an invisible grab area at grid crossing points that

packages/golden-layout/src/__tests__/intersection-drag.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,63 @@ describe('intersection splitter drag', () => {
417417
}
418418
});
419419

420+
it('creates a handle for a perpendicular splitter nested deeper than one level', async () => {
421+
const restoreMocks = setupDimensionMocks();
422+
423+
try {
424+
// The bottom row holds a left column whose first child is itself a row.
425+
// That inner row's vertical splitter is two levels below the root column's
426+
// top/bottom bar yet its top still touches it, so the bar owner (the root
427+
// column) must create a crossing handle for it.
428+
layout = await createLayout({
429+
content: [
430+
{
431+
type: 'column',
432+
content: [
433+
{ type: 'component', componentName: 'testComponent' },
434+
{
435+
type: 'row',
436+
content: [
437+
{
438+
type: 'column',
439+
content: [
440+
{
441+
type: 'row',
442+
content: [
443+
{ type: 'component', componentName: 'testComponent' },
444+
{ type: 'component', componentName: 'testComponent' },
445+
],
446+
},
447+
{ type: 'component', componentName: 'testComponent' },
448+
],
449+
},
450+
{ type: 'component', componentName: 'testComponent' },
451+
],
452+
},
453+
],
454+
},
455+
],
456+
});
457+
458+
const rootColumn = verifyPath('column', layout) as any;
459+
expect(rootColumn).toBeDefined();
460+
461+
await new Promise<void>(resolve => {
462+
window.requestAnimationFrame(() => resolve());
463+
});
464+
465+
// Handles owned by the root column are appended directly into its element.
466+
// It owns one for the bottom row's own vertical splitter (left column |
467+
// right component) and one for the deeply nested inner-row splitter.
468+
const rootOwnedHandles = rootColumn.element.children(
469+
'.lm_intersection_splitter'
470+
);
471+
expect(rootOwnedHandles.length).toBe(2);
472+
} finally {
473+
restoreMocks();
474+
}
475+
});
476+
420477
it('preserves all intersection handles after normal vertical and horizontal splitter drags', async () => {
421478
const restoreMocks = setupDimensionMocks();
422479

@@ -600,4 +657,75 @@ describe('intersection splitter drag', () => {
600657
restoreMocks();
601658
}
602659
});
660+
661+
it('stretches the stem line with a transform (not box size) and clears it on stop', async () => {
662+
const restoreMocks = setupDimensionMocks();
663+
664+
try {
665+
layout = await createLayout({
666+
content: [
667+
{
668+
type: 'column',
669+
content: [
670+
{ type: 'component', componentName: 'testComponent' },
671+
{
672+
type: 'row',
673+
content: [
674+
{
675+
type: 'column',
676+
content: [
677+
{ type: 'component', componentName: 'testComponent' },
678+
{ type: 'component', componentName: 'testComponent' },
679+
],
680+
},
681+
{ type: 'component', componentName: 'testComponent' },
682+
],
683+
},
684+
],
685+
},
686+
],
687+
});
688+
689+
const bottomRow = verifyPath('column.1.row', layout) as any;
690+
expect(bottomRow).toBeDefined();
691+
692+
const intersectionHandle = bottomRow.element
693+
.find('.lm_intersection_splitter')
694+
.first();
695+
expect(intersectionHandle.length).toBe(1);
696+
697+
// The stem line is the vertical splitter inside the bottom row.
698+
const stemLine = bottomRow.element.find('.lm_splitter.lm_vertical');
699+
expect(stemLine.length).toBe(1);
700+
const stemEl = stemLine[0] as HTMLElement;
701+
702+
const startX = 100;
703+
const startY = 100;
704+
const mousedown = $.Event('mousedown') as JQuery.TriggeredEvent;
705+
mousedown.pageX = startX;
706+
mousedown.pageY = startY;
707+
mousedown.button = 0;
708+
intersectionHandle.trigger(mousedown);
709+
710+
const mousemove = $.Event('mousemove') as JQuery.TriggeredEvent;
711+
mousemove.pageX = startX - 40;
712+
mousemove.pageY = startY - 40;
713+
$(document).trigger(mousemove);
714+
715+
// While dragging, the stem is stretched via a scale transform - never by
716+
// mutating its box size, which would reflow sibling panes and headers.
717+
expect(stemEl.style.transform).toContain('scale');
718+
expect(stemEl.style.width).toBe('');
719+
720+
$(document).trigger('mouseup');
721+
await new Promise<void>(resolve => {
722+
window.requestAnimationFrame(() => resolve());
723+
});
724+
725+
// The transform is cleared once the drag stops.
726+
expect(stemEl.style.transform).toBe('');
727+
} finally {
728+
restoreMocks();
729+
}
730+
});
603731
});

0 commit comments

Comments
 (0)