Merge feat/dnd-kit-grid into feat/widget-modernization

This commit is contained in:
Thomas Camlong committed 2026-08-10 12:02:06 +02:00
commit b6053a7b31
6 files changed
+102 -21

No files matched your search

+1 -1
View File
@@ -134,7 +134,7 @@ For example, a 12-column layout with a two-column left rail leaves ten columns f
Enabling or widening a rail reflows canvas items into the remaining columns.
Disabling a rail moves its contents back into the next available canvas positions.
Rails fill the available window height and remain fixed while the page scrolls.
Rails fill the available window height and remain fixed while the page scrolls. They do not grow or scroll independently.
Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode, where each rail shows its name and boundary.
#### Breakpoint
@@ -21,10 +21,7 @@
height: var(--board-gutter-viewport-height);
padding-top: 24px;
margin-top: -24px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
overflow: clip;
}
.gutter[data-board-editing="true"] {
@@ -116,11 +116,6 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr
pointer-events: none;
}
.board-grid-resize-outline[data-grid-resize-valid="false"] {
background: color-mix(in srgb, var(--mantine-color-red-filled) 8%, transparent);
border-color: var(--mantine-color-red-filled);
}
.board-grid-editor[data-dnd-drop-target="true"] {
outline: 2px solid color-mix(in srgb, var(--mantine-primary-color-filled) 60%, transparent);
outline-offset: 4px;
@@ -59,6 +59,7 @@ interface GridEditorProps {
sectionId: string;
columnCount: number;
rowCount: number;
maxRowCount: number | null;
placements: readonly SectionGridPlacement[];
className: string;
}
@@ -707,7 +708,14 @@ const GridCollisionSynchronizer = ({ interaction }: { interaction: GridInteracti
};
/** A section-local dnd-kit canvas registered with the board transaction. */
export default function GridEditor({ sectionId, columnCount, rowCount, placements, className }: GridEditorProps) {
export default function GridEditor({
sectionId,
columnCount,
rowCount,
maxRowCount,
placements,
className,
}: GridEditorProps) {
const t = useI18n();
const { section, items, innerSections } = useSectionContext();
const { interaction, registerGrid, getDepth, isDropTargetEligible } = useBoardGridEditor();
@@ -744,7 +752,7 @@ export default function GridEditor({ sectionId, columnCount, rowCount, placement
interaction && (interaction.sourceGridId === sectionId || interaction.targetGridId === sectionId);
const previewRowCount = getLayoutRowCount(previewGrid?.placements ?? renderPlacements);
const renderedRowCount =
section.kind !== "container" && participatesInInteraction
maxRowCount === null && participatesInInteraction
? Math.max(rowCount, previewRowCount) + (growsDuringDrag ? 1 : 0)
: rowCount;
@@ -775,14 +783,14 @@ export default function GridEditor({ sectionId, columnCount, rowCount, placement
return registerGrid({
id: sectionId,
columnCount,
maxRowCount: section.kind === "container" ? rowCount : null,
maxRowCount,
parentGridId,
ownerPlacementId,
placements,
depth,
element,
});
}, [columnCount, depth, ownerPlacementId, parentGridId, placements, registerGrid, rowCount, section.kind, sectionId]);
}, [columnCount, depth, maxRowCount, ownerPlacementId, parentGridId, placements, registerGrid, sectionId]);
useLayoutEffect(() => {
const viewport = gridRef.current?.parentElement;
@@ -824,7 +832,7 @@ export default function GridEditor({ sectionId, columnCount, rowCount, placement
placement={placement}
label={label}
columnCount={columnCount}
maxRowCount={section.kind === "container" ? rowCount : null}
maxRowCount={maxRowCount}
/>
);
})}
@@ -1044,6 +1052,7 @@ const GridResizeHandles = ({
y: number;
direction: GridResizeDirection;
visualScale: { x: number; y: number };
placement: SectionGridPlacement;
} | null>(null);
const start = (direction: GridResizeDirection) =>
@@ -1077,7 +1086,14 @@ const GridResizeHandles = ({
cancelResize();
return;
}
pointerStartRef.current = { id: event.pointerId, x: event.clientX, y: event.clientY, direction, visualScale };
pointerStartRef.current = {
id: event.pointerId,
x: event.clientX,
y: event.clientY,
direction,
visualScale,
placement: { ...placement },
};
};
const handlePointerMove = (event: ReactPointerEvent<HTMLSpanElement>) => {
@@ -1095,14 +1111,14 @@ const GridResizeHandles = ({
const valid = update(started.direction, snappedDelta.x, snappedDelta.y);
setContinuousResize({
placement: getContinuousResizePlacement({
placement,
placement: started.placement,
direction: started.direction,
deltaColumns: continuousDelta.x,
deltaRows: continuousDelta.y,
columnCount,
maxRowCount,
minWidth: placement.minW ?? 1,
minHeight: placement.minH ?? 1,
minWidth: started.placement.minW ?? 1,
minHeight: started.placement.minH ?? 1,
}),
valid,
});
@@ -150,6 +150,7 @@ export const SectionGrid = ({
const minimumViewportRowCount = useMinimumViewportRowCount(section.kind === "empty", canvasScale);
const contentRowCount = Math.max(1, getLayoutRowCount(displayPlacements));
const rowCount = Math.max(contentRowCount, requestedRowCount, minimumViewportRowCount);
const maxRowCount = section.kind === "container" || railPlacement !== "main" ? rowCount : null;
const logicalWidth = getLogicalTrackSize(columnCount);
const logicalHeight = getLogicalTrackSize(rowCount);
const canvasAttributes = isEditMode
@@ -164,7 +165,7 @@ export const SectionGrid = ({
innerSections: displayedInnerSections,
integrations,
columnCount,
maxRowCount: section.kind === "container" ? rowCount : null,
maxRowCount,
announce,
}}
>
@@ -182,6 +183,7 @@ export const SectionGrid = ({
sectionId={section.id}
columnCount={columnCount}
rowCount={rowCount}
maxRowCount={maxRowCount}
placements={displayPlacements}
className={combineClasses("board-grid-editor", classes.editorGrid)}
/>
+72 -1
View File
@@ -95,7 +95,8 @@ describe("Board grid", () => {
await expect(rail).toBeVisible();
await expect(rail).toHaveAttribute("data-board-gutter", "left");
await expect(railRootSection).toHaveAttribute("data-rail-placement", "left");
await expect(rail).toHaveCSS("overflow-x", "hidden");
await expect(rail).toHaveCSS("overflow-x", "clip");
await expect(rail).toHaveCSS("overflow-y", "clip");
const initialRailBox = await expectBoundingBoxAsync(rail);
const initialRailRootBox = await expectBoundingBoxAsync(railRootSection);
expect(initialRailRootBox.width).toBeCloseTo(initialRailBox.width, 1);
@@ -240,6 +241,7 @@ describe("Board grid", () => {
const viewMainSectionBox = await expectBoundingBoxAsync(mainSection);
const viewFirstItemBox = await expectBoundingBoxAsync(logicalTile);
const viewScrollY = await page.evaluate(() => window.scrollY);
runtimeResources.enterEditMode();
await editToggle.click();
await expect(page.locator(`[data-grid-runtime="${gridRuntimeMarker}"]`)).toHaveCount(3);
@@ -285,6 +287,39 @@ describe("Board grid", () => {
await page.mouse.up();
await expect.poll(async () => await readEditorGridSnapshotAsync(railGrid)).toEqual(railSnapshot);
await rail.locator('[data-e2e-rail-overflow-fixture="true"]').evaluate((element) => element.remove());
await rail.evaluate((element) => {
const overflowFixture = document.createElement("div");
overflowFixture.dataset.e2eRailOverflowFixture = "true";
overflowFixture.style.position = "absolute";
overflowFixture.style.insetBlockStart = "100%";
overflowFixture.style.width = "1px";
overflowFixture.style.height = "500px";
overflowFixture.style.pointerEvents = "none";
element.appendChild(overflowFixture);
});
await expect
.poll(async () => await rail.evaluate((element) => element.scrollHeight - element.clientHeight))
.toBeGreaterThan(0);
const fixedRailGridHeight = (await expectBoundingBoxAsync(railGrid)).height;
const fixedRailHeight = (await expectBoundingBoxAsync(rail)).height;
const verticalRailItemBox = await expectBoundingBoxAsync(railItem);
await page.mouse.move(
verticalRailItemBox.x + verticalRailItemBox.width / 2,
verticalRailItemBox.y + verticalRailItemBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(verticalRailItemBox.x + verticalRailItemBox.width / 2 + 8, verticalRailItemBox.y);
await expect(railItem).toHaveAttribute("data-dnd-drag-source", "true");
await page.mouse.move(railBox.x + railBox.width / 2, railBox.y + railBox.height - 2, { steps: 12 });
await page.waitForTimeout(250);
expect(await rail.evaluate((element) => element.scrollTop)).toBe(0);
expect((await expectBoundingBoxAsync(rail)).height).toBeCloseTo(fixedRailHeight, 1);
expect((await expectBoundingBoxAsync(railGrid)).height).toBeCloseTo(fixedRailGridHeight, 1);
await page.keyboard.press("Escape");
await page.mouse.up();
await expect.poll(async () => await readEditorGridSnapshotAsync(railGrid)).toEqual(railSnapshot);
await rail.locator('[data-e2e-rail-overflow-fixture="true"]').evaluate((element) => element.remove());
await page.evaluate((scrollY) => window.scrollTo({ top: scrollY, behavior: "auto" }), viewScrollY);
await expect(logicalTile).toHaveCSS("overflow-x", "hidden");
await expect(logicalTile).toHaveCSS("overflow-y", "auto");
await logicalTile.evaluate((element) => {
@@ -767,6 +802,42 @@ describe("Board grid", () => {
await expect(eastResizeHandle).not.toHaveAttribute("data-grid-resize-disabled");
await resizeLocatorByImmediateReleaseAsync(page, eastResizeHandle, logicalCellPitch * canvasScale, 0);
await expect(containerSection).toHaveAttribute("data-grid-w", "2");
await expectGridEntryGeometrySettledAsync(containerSection);
await expect(eastResizeHandle).not.toHaveAttribute("data-grid-resize-disabled");
const expandingEastResizeBox = await expectBoundingBoxAsync(eastResizeHandle);
const expandingEastResizeStart = {
x: expandingEastResizeBox.x + expandingEastResizeBox.width / 2,
y: expandingEastResizeBox.y + expandingEastResizeBox.height / 2,
};
const expandingContainerBox = await expectBoundingBoxAsync(containerSection);
const eastHandleGrabOffset = expandingContainerBox.x + expandingContainerBox.width - expandingEastResizeStart.x;
const continuousExpansion = logicalCellPitch * canvasScale * 1.2;
await page.mouse.move(expandingEastResizeStart.x, expandingEastResizeStart.y);
await page.mouse.down();
await expect(page.locator("body")).toHaveAttribute("data-board-grid-interacting", "resize");
await page.mouse.move(expandingEastResizeStart.x + continuousExpansion, expandingEastResizeStart.y, {
steps: 12,
});
await expect(resizeOutline).toBeVisible();
const expandedOutlineBox = await expectBoundingBoxAsync(resizeOutline);
expect(expandedOutlineBox.x + expandedOutlineBox.width).toBeCloseTo(
expandingEastResizeStart.x + continuousExpansion + eastHandleGrabOffset,
1,
);
expect(
await resizeOutline.evaluate((element) => {
const probe = document.createElement("span");
probe.style.color = "var(--mantine-primary-color-filled)";
element.appendChild(probe);
const usesPrimaryColor = getComputedStyle(element).borderTopColor === getComputedStyle(probe).color;
probe.remove();
return usesPrimaryColor;
}),
).toBe(true);
await page.keyboard.press("Escape");
await page.mouse.up();
await expect(containerSection).toHaveAttribute("data-grid-w", "2");
const northResizeHandle = containerSection.locator(':scope > [data-grid-resize-handle="n"]');
await expectTargetSizeAsync(northResizeHandle);