fix: contain collapsed layout and widget overflow
This commit is contained in:
1 parent
498481d539
commit
90cf0e1127
5 files changed
+124
-12
No files matched your search
@@ -77,7 +77,7 @@ export const getCollapsedDisplayLayout = <TPlacement extends GridPlacement>(
|
||||
: placement,
|
||||
);
|
||||
|
||||
return reflowDisplayLayout(displayPlacements);
|
||||
return reflowCollapsedDisplayLayout(normalized, displayPlacements, collapsedItemIds, collapsedRowCount);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -118,19 +118,71 @@ const findFirstAvailableRow = (candidate: GridPlacement, placed: readonly GridPl
|
||||
}
|
||||
};
|
||||
|
||||
const reflowDisplayLayout = <TPlacement extends GridPlacement>(placements: readonly TPlacement[]): TPlacement[] => {
|
||||
assertUniqueIds(placements);
|
||||
const reflowCollapsedDisplayLayout = <TPlacement extends GridPlacement>(
|
||||
expandedPlacements: readonly TPlacement[],
|
||||
displayPlacements: readonly TPlacement[],
|
||||
collapsedItemIds: ReadonlySet<string>,
|
||||
collapsedRowCount: number,
|
||||
): TPlacement[] => {
|
||||
assertUniqueIds(expandedPlacements);
|
||||
const displayById = new Map(displayPlacements.map((placement) => [placement.id, placement]));
|
||||
const collapsedPlacements = expandedPlacements.filter((placement) => collapsedItemIds.has(placement.id));
|
||||
const placed: TPlacement[] = [];
|
||||
|
||||
for (const candidate of placements.toSorted(comparePlacements)) {
|
||||
for (const expandedPlacement of expandedPlacements.toSorted(comparePlacements)) {
|
||||
const candidate = getRequiredPlacement(displayById, expandedPlacement.id);
|
||||
const verticalShift = getCollapsedVerticalShift(expandedPlacement, collapsedPlacements, collapsedRowCount);
|
||||
placed.push({
|
||||
...candidate,
|
||||
y: findFirstAvailableRow(candidate, placed),
|
||||
y: findFirstAvailableRowAtOrAfter(candidate, placed, Math.max(0, candidate.y - verticalShift)),
|
||||
});
|
||||
}
|
||||
|
||||
const byId = new Map(placed.map((placement) => [placement.id, placement]));
|
||||
return placements.map((placement) => getRequiredPlacement(byId, placement.id));
|
||||
return displayPlacements.map((placement) => getRequiredPlacement(byId, placement.id));
|
||||
};
|
||||
|
||||
const getCollapsedVerticalShift = (
|
||||
candidate: GridPlacement,
|
||||
collapsedPlacements: readonly GridPlacement[],
|
||||
collapsedRowCount: number,
|
||||
) => {
|
||||
const removedIntervals = collapsedPlacements
|
||||
.filter(
|
||||
(collapsed) =>
|
||||
collapsed.id !== candidate.id &&
|
||||
collapsed.y + collapsed.h <= candidate.y &&
|
||||
doGridPlacementsOverlapHorizontally(candidate, collapsed),
|
||||
)
|
||||
.map((collapsed) => ({ start: collapsed.y + collapsedRowCount, end: collapsed.y + collapsed.h }))
|
||||
.toSorted((first, second) => first.start - second.start || first.end - second.end);
|
||||
|
||||
let shift = 0;
|
||||
let intervalEnd = Number.NEGATIVE_INFINITY;
|
||||
for (const interval of removedIntervals) {
|
||||
const start = Math.max(interval.start, intervalEnd);
|
||||
if (interval.end > start) shift += interval.end - start;
|
||||
intervalEnd = Math.max(intervalEnd, interval.end);
|
||||
}
|
||||
return shift;
|
||||
};
|
||||
|
||||
const doGridPlacementsOverlapHorizontally = (first: GridPlacement, second: GridPlacement) =>
|
||||
first.x < second.x + second.w && first.x + first.w > second.x;
|
||||
|
||||
const findFirstAvailableRowAtOrAfter = (
|
||||
candidate: GridPlacement,
|
||||
placed: readonly GridPlacement[],
|
||||
minimumRow: number,
|
||||
) => {
|
||||
let y = minimumRow;
|
||||
for (;;) {
|
||||
const atRow = { ...candidate, y };
|
||||
const collisions = placed.filter((placement) => doGridPlacementsOverlap(atRow, placement));
|
||||
if (collisions.length === 0) return y;
|
||||
|
||||
y = Math.max(...collisions.map((placement) => placement.y + placement.h));
|
||||
}
|
||||
};
|
||||
|
||||
const comparePlacements = (first: GridPlacement, second: GridPlacement) =>
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("dashboard layout reflow", () => {
|
||||
expect(hasOverlap(result)).toBe(false);
|
||||
});
|
||||
|
||||
test("shrinks a collapsed inline container and pulls neighbors up", () => {
|
||||
test("shrinks a collapsed inline container without compacting unrelated columns", () => {
|
||||
const result = getCollapsedDisplayLayout(
|
||||
[
|
||||
{ id: "section", x: 0, y: 0, w: 2, h: 3 },
|
||||
@@ -44,7 +44,41 @@ describe("dashboard layout reflow", () => {
|
||||
expect(result).toEqual([
|
||||
{ id: "section", x: 0, y: 0, w: 2, h: 0.5 },
|
||||
{ id: "below", x: 0, y: 0.5, w: 2, h: 1 },
|
||||
{ id: "other-lane", x: 2, y: 0, w: 1, h: 1 },
|
||||
{ id: "other-lane", x: 2, y: 4, w: 1, h: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves intentional gaps below a collapsed container", () => {
|
||||
const result = getCollapsedDisplayLayout(
|
||||
[
|
||||
{ id: "section", x: 0, y: 1, w: 2, h: 3 },
|
||||
{ id: "below", x: 0, y: 6, w: 2, h: 1 },
|
||||
{ id: "above", x: 0, y: 0, w: 2, h: 1 },
|
||||
],
|
||||
{ columnCount: 2, collapsedItemIds: new Set(["section"]) },
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: "section", x: 0, y: 1, w: 2, h: 0.5 },
|
||||
{ id: "below", x: 0, y: 3.5, w: 2, h: 1 },
|
||||
{ id: "above", x: 0, y: 0, w: 2, h: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("does not double-count side-by-side collapsed space", () => {
|
||||
const result = getCollapsedDisplayLayout(
|
||||
[
|
||||
{ id: "left", x: 0, y: 0, w: 1, h: 3 },
|
||||
{ id: "right", x: 1, y: 0, w: 1, h: 3 },
|
||||
{ id: "below", x: 0, y: 3, w: 2, h: 1 },
|
||||
],
|
||||
{ columnCount: 2, collapsedItemIds: new Set(["left", "right"]) },
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: "left", x: 0, y: 0, w: 1, h: 0.5 },
|
||||
{ id: "right", x: 1, y: 0, w: 1, h: 0.5 },
|
||||
{ id: "below", x: 0, y: 0.5, w: 2, h: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ body[data-board-grid-interacting="drag"] .board-grid-placeholder {
|
||||
}
|
||||
|
||||
.board-grid-content-mount,
|
||||
.board-grid-item-content,
|
||||
.board-grid-item-content > [data-editor-grid-entry] {
|
||||
.board-grid-portal-content,
|
||||
.board-grid-portal-content > [data-editor-grid-entry] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
|
||||
@@ -50,7 +50,7 @@ export const BoardGridPortalHost = ({ children }: PropsWithChildren) => {
|
||||
if (existing) return existing;
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.className = "board-grid-item-content";
|
||||
container.className = "board-grid-portal-content";
|
||||
container.dataset.boardGridPortal = id;
|
||||
containersRef.current.set(id, container);
|
||||
setContainers(new Map(containersRef.current));
|
||||
|
||||
+27
-1
@@ -126,7 +126,8 @@ describe("Board grid", () => {
|
||||
const logicalTile = firstItem.locator("[data-grid-item-content]");
|
||||
await expect(logicalTile).toHaveCount(1);
|
||||
await expectFixedLogicalTileAsync(logicalTile);
|
||||
await expect(logicalTile).toHaveCSS("overflow", "hidden");
|
||||
await expect(logicalTile).toHaveCSS("overflow-x", "hidden");
|
||||
await expect(logicalTile).toHaveCSS("overflow-y", "auto");
|
||||
await page.addStyleTag({ content: ".e2e-widget-overflow { overflow: visible; }" });
|
||||
await expect(secondItem.locator("[data-grid-item-content]")).toHaveCSS("overflow", "visible");
|
||||
await expect(canvas).toHaveAttribute("data-canvas-overflow", "false");
|
||||
@@ -230,6 +231,30 @@ describe("Board grid", () => {
|
||||
await expect(page.locator(`[data-grid-runtime="${gridRuntimeMarker}"]`)).toHaveCount(3);
|
||||
await expect(page.getByTestId("board-grid-editor-loading")).toHaveCount(0);
|
||||
await expect(page.getByTestId("board-canvas-row-count-button")).toHaveCount(0);
|
||||
await expect(logicalTile).toHaveCSS("overflow-x", "hidden");
|
||||
await expect(logicalTile).toHaveCSS("overflow-y", "auto");
|
||||
await logicalTile.evaluate((element) => {
|
||||
const overflowFixture = document.createElement("div");
|
||||
overflowFixture.dataset.e2eWidgetOverflowFixture = "true";
|
||||
overflowFixture.style.width = "800px";
|
||||
overflowFixture.style.minWidth = "800px";
|
||||
overflowFixture.style.height = "800px";
|
||||
overflowFixture.style.minHeight = "800px";
|
||||
overflowFixture.style.flex = "0 0 800px";
|
||||
element.appendChild(overflowFixture);
|
||||
});
|
||||
await expect
|
||||
.poll(async () => await logicalTile.evaluate((element) => element.scrollWidth - element.clientWidth))
|
||||
.toBeGreaterThan(0);
|
||||
await expect
|
||||
.poll(async () => await logicalTile.evaluate((element) => element.scrollHeight - element.clientHeight))
|
||||
.toBeGreaterThan(0);
|
||||
await logicalTile.evaluate((element) => {
|
||||
element.scrollTop = 100;
|
||||
});
|
||||
await expect.poll(async () => await logicalTile.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
||||
await expectDocumentNotHorizontallyScrollableAsync(page);
|
||||
await logicalTile.locator('[data-e2e-widget-overflow-fixture="true"]').evaluate((element) => element.remove());
|
||||
await expectBoundingBoxToMatchAsync(mainSection, viewMainSectionBox);
|
||||
await expectBoundingBoxToMatchAsync(logicalTile, viewFirstItemBox);
|
||||
await runtimeResources.waitForQuietAsync();
|
||||
@@ -691,6 +716,7 @@ describe("Board grid", () => {
|
||||
await dragLocatorByAsync(page, northResizeHandle, 0, logicalCellPitch * canvasScale);
|
||||
await expect(containerSection).toHaveAttribute("data-grid-y", "1");
|
||||
await expect(containerSection).toHaveAttribute("data-grid-h", "1");
|
||||
await expectGridEntryGeometrySettledAsync(containerSection);
|
||||
|
||||
const minimumSectionBox = await expectBoundingBoxAsync(containerSection);
|
||||
const minimumHandleBox = await expectBoundingBoxAsync(northResizeHandle);
|
||||
|
||||
Reference in new issue
Block a user