Merge grid geometry fixes into dnd kit
This commit is contained in:
13 files changed
+142
-57
No files matched your search
@@ -38,7 +38,8 @@ Underscores and hyphen-minus are allowed.
|
||||
|
||||
The column count defines how many fixed-width cells are available in each row.
|
||||
One cell contains a `200 × 200` logical-pixel tile.
|
||||
Homarr scales the whole board uniformly instead of changing widget proportions, with horizontal scrolling when further shrinking would make controls too small.
|
||||
Homarr scales the whole board uniformly instead of changing widget proportions, and always fits it to the available width without horizontal scrolling.
|
||||
The main canvas stays at least one viewport tall and grows automatically while you drag or resize items near its lower edge.
|
||||
|
||||
### Public
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ The board uses fixed logical units:
|
||||
Items keep the same logical dimensions at every viewport. The viewport reserves
|
||||
the painted size while the inner canvas remains at its logical dimensions, so a
|
||||
widget's `clientWidth`, `clientHeight`, and `ResizeObserver` measurements stay
|
||||
stable.
|
||||
stable. The visual canvas always fits the available width, the main root keeps
|
||||
at least one viewport of height, and edit-mode previews extend it downward as
|
||||
needed.
|
||||
|
||||
The read-only renderer should:
|
||||
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
export const LOGICAL_GRID_CELL_SIZE = 200;
|
||||
export const LOGICAL_GRID_GAP = 24;
|
||||
export const LOGICAL_GRID_PITCH = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP;
|
||||
export const MIN_ACCESSIBLE_CANVAS_SCALE = 0.75;
|
||||
|
||||
export const COLLAPSED_SECTION_ROW_COUNT = 0.5;
|
||||
@@ -21,6 +21,13 @@ export const getLogicalItemStyle = ({ x, y, w, h }: GridCoordinates): CSSPropert
|
||||
export const getLayoutRowCount = (placements: readonly GridCoordinates[]) =>
|
||||
placements.reduce((rowCount, placement) => Math.max(rowCount, placement.y + placement.h), 0);
|
||||
|
||||
export const getGridRowCountForVisualHeight = (visualHeight: number, visualScale: number) => {
|
||||
if (!Number.isFinite(visualHeight) || visualHeight <= 0) return 1;
|
||||
if (!Number.isFinite(visualScale) || visualScale <= 0) return 1;
|
||||
|
||||
return Math.max(1, Math.ceil((visualHeight / visualScale + LOGICAL_GRID_GAP) / LOGICAL_GRID_PITCH));
|
||||
};
|
||||
|
||||
export const normalizeGridPlacement = <TPlacement extends GridPlacement>(
|
||||
placement: TPlacement,
|
||||
columnCount: number,
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.viewport[data-board-hydrated="false"] {
|
||||
|
||||
@@ -5,11 +5,8 @@ import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
|
||||
import { MIN_ACCESSIBLE_CANVAS_SCALE } from "./constants";
|
||||
import classes from "./scaled-board-canvas.module.css";
|
||||
|
||||
export { MIN_ACCESSIBLE_CANVAS_SCALE };
|
||||
|
||||
const BoardCanvasScaleContext = createContext(1);
|
||||
|
||||
export const useBoardCanvasScale = () => useContext(BoardCanvasScaleContext);
|
||||
@@ -18,7 +15,7 @@ export const calculateBoardCanvasScale = (availableWidth: number, logicalWidth:
|
||||
if (!Number.isFinite(availableWidth) || !Number.isFinite(logicalWidth)) return 1;
|
||||
if (availableWidth <= 0 || logicalWidth <= 0) return 1;
|
||||
|
||||
return Math.max(MIN_ACCESSIBLE_CANVAS_SCALE, availableWidth / logicalWidth);
|
||||
return availableWidth / logicalWidth;
|
||||
};
|
||||
|
||||
export const calculateBoardUiScale = (canvasScale: number) => {
|
||||
@@ -36,9 +33,8 @@ interface ScaledBoardCanvasProps {
|
||||
|
||||
/**
|
||||
* Scales the complete board as one surface while keeping widget layout in
|
||||
* fixed logical pixels. The scale floor deliberately allows horizontal
|
||||
* overflow so browser zoom still magnifies content instead of being cancelled
|
||||
* by responsive downscaling.
|
||||
* fixed logical pixels. The complete canvas always fits its viewport so board
|
||||
* content can never widen the document or require horizontal scrolling.
|
||||
*/
|
||||
export const ScaledBoardCanvas = ({
|
||||
logicalWidth,
|
||||
@@ -62,6 +58,7 @@ export const ScaledBoardCanvas = ({
|
||||
[logicalWidth, resolvedAvailableWidth],
|
||||
);
|
||||
const uiScale = calculateBoardUiScale(scale);
|
||||
const inverseScale = scale > 0 ? 1 / scale : 1;
|
||||
const visualWidth = logicalWidth * scale;
|
||||
const hasHorizontalOverflow = resolvedAvailableWidth > 0 && visualWidth - resolvedAvailableWidth > 0.5;
|
||||
|
||||
@@ -84,6 +81,7 @@ export const ScaledBoardCanvas = ({
|
||||
className={classes.canvas}
|
||||
w={logicalWidth}
|
||||
style={{
|
||||
"--board-canvas-inverse-scale": inverseScale,
|
||||
"--board-canvas-ui-scale": uiScale,
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getLayoutRowCount, getLogicalItemStyle, getLogicalTrackSize, normalizeGridPlacement } from "../index";
|
||||
import {
|
||||
getGridRowCountForVisualHeight,
|
||||
getLayoutRowCount,
|
||||
getLogicalItemStyle,
|
||||
getLogicalTrackSize,
|
||||
normalizeGridPlacement,
|
||||
} from "../index";
|
||||
|
||||
describe("fixed dashboard geometry", () => {
|
||||
test("keeps a 1x1 item exactly 200px with a fixed 24px gap", () => {
|
||||
@@ -24,4 +30,12 @@ describe("fixed dashboard geometry", () => {
|
||||
expect(placement).toEqual({ id: "wide", x: 0, y: 0, w: 4, h: 2 });
|
||||
expect(getLayoutRowCount([placement, { x: 0, y: 4, w: 1, h: 3 }])).toBe(7);
|
||||
});
|
||||
|
||||
test("derives enough logical rows to fill the visual viewport at any canvas scale", () => {
|
||||
expect(getGridRowCountForVisualHeight(1200, 1)).toBe(6);
|
||||
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 1))).toBeGreaterThanOrEqual(1200);
|
||||
|
||||
expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(11);
|
||||
expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 0.5)) * 0.5).toBeGreaterThanOrEqual(1200);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { calculateBoardCanvasScale, calculateBoardUiScale, MIN_ACCESSIBLE_CANVAS_SCALE } from "../scaled-board-canvas";
|
||||
import { calculateBoardCanvasScale, calculateBoardUiScale } from "../scaled-board-canvas";
|
||||
|
||||
describe("board canvas scale policy", () => {
|
||||
test("fits the logical canvas exactly when the viewport can preserve the minimum scale", () => {
|
||||
test("fits the logical canvas exactly", () => {
|
||||
const scale = calculateBoardCanvasScale(840, 1000);
|
||||
|
||||
expect(scale).toBe(0.84);
|
||||
@@ -14,16 +14,16 @@ describe("board canvas scale policy", () => {
|
||||
expect(calculateBoardCanvasScale(1800, 1000)).toBe(1.8);
|
||||
});
|
||||
|
||||
test("stops counter-scaling at 0.75 and lets the canvas overflow", () => {
|
||||
test("keeps fitting narrow viewports without horizontal overflow", () => {
|
||||
const availableWidth = 600;
|
||||
const logicalWidth = 1000;
|
||||
const scale = calculateBoardCanvasScale(availableWidth, logicalWidth);
|
||||
|
||||
expect(scale).toBe(MIN_ACCESSIBLE_CANVAS_SCALE);
|
||||
expect(logicalWidth * scale).toBeGreaterThan(availableWidth);
|
||||
expect(scale).toBe(0.6);
|
||||
expect(logicalWidth * scale).toBe(availableWidth);
|
||||
});
|
||||
|
||||
test("keeps content materially magnified at 200% effective zoom", () => {
|
||||
test("keeps the board fitted at 200% effective zoom", () => {
|
||||
const logicalWidth = 1696;
|
||||
const normalAvailableWidth = 1872;
|
||||
const normalScale = calculateBoardCanvasScale(normalAvailableWidth, logicalWidth);
|
||||
@@ -31,8 +31,8 @@ describe("board canvas scale policy", () => {
|
||||
const normalPaintedCellSize = 200 * normalScale;
|
||||
const zoomedPaintedCellSize = 200 * zoomedScale * 2;
|
||||
|
||||
expect(zoomedScale).toBe(MIN_ACCESSIBLE_CANVAS_SCALE);
|
||||
expect(zoomedPaintedCellSize).toBeGreaterThan(normalPaintedCellSize * 1.25);
|
||||
expect(zoomedScale).toBeCloseTo(normalScale / 2);
|
||||
expect(zoomedPaintedCellSize).toBeCloseTo(normalPaintedCellSize);
|
||||
});
|
||||
|
||||
test.each([
|
||||
|
||||
@@ -18,7 +18,7 @@ export const BoardEmptySection = ({ section, columnCount, requestedRowCount, rai
|
||||
const [isEditMode] = useEditMode();
|
||||
const t = useScopedI18n("board.landmark");
|
||||
|
||||
if (totalLength === 0 && !isEditMode && requestedRowCount === 0) return null;
|
||||
if (totalLength === 0 && !isEditMode && requestedRowCount === 0 && railPlacement !== "main") return null;
|
||||
|
||||
return (
|
||||
<SectionGrid
|
||||
|
||||
@@ -86,6 +86,10 @@ body[data-board-grid-interacting="drag"] .board-grid-placeholder {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="true"] > .board-grid-content-mount {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.board-grid-placeholder {
|
||||
z-index: 0;
|
||||
background: color-mix(in srgb, var(--mantine-primary-color-filled) 16%, transparent);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CollisionPriority } from "@dnd-kit/abstract";
|
||||
import { pointerIntersection } from "@dnd-kit/collision";
|
||||
import type { DragDropManager, Draggable as DomDraggable } from "@dnd-kit/dom";
|
||||
import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints, PointerSensor } from "@dnd-kit/dom";
|
||||
import { RestrictToElement } from "@dnd-kit/dom/modifiers";
|
||||
import { getEventCoordinates, getFrameTransform, isPointerEvent } from "@dnd-kit/dom/utilities";
|
||||
import type {
|
||||
BeforeDragStartEvent,
|
||||
@@ -720,6 +721,7 @@ export default function GridEditor({ sectionId, columnCount, rowCount, placement
|
||||
if (!previewGrid || !interaction) return placements;
|
||||
|
||||
if (interaction.sourceGridId === sectionId) {
|
||||
if (interaction.mode === "resize") return previewGrid.placements;
|
||||
const original = controlledById.get(interaction.activeId);
|
||||
if (!original) return previewGrid.placements;
|
||||
return previewGrid.placements.some((placement) => placement.id === interaction.activeId)
|
||||
@@ -734,9 +736,16 @@ export default function GridEditor({ sectionId, columnCount, rowCount, placement
|
||||
);
|
||||
const targetPlacement =
|
||||
interaction?.valid && interaction.targetGridId === sectionId ? interaction.targetPlacement : null;
|
||||
const growsDuringDrag =
|
||||
interaction?.mode === "drag" &&
|
||||
(interaction.targetGridId === sectionId ||
|
||||
(interaction.targetGridId === null && interaction.sourceGridId === sectionId));
|
||||
const participatesInInteraction =
|
||||
interaction && (interaction.sourceGridId === sectionId || interaction.targetGridId === sectionId);
|
||||
const previewRowCount = getLayoutRowCount(previewGrid?.placements ?? renderPlacements);
|
||||
const renderedRowCount =
|
||||
section.kind !== "container" && interaction?.mode === "resize"
|
||||
? Math.max(rowCount, getLayoutRowCount(renderPlacements))
|
||||
section.kind !== "container" && participatesInInteraction
|
||||
? Math.max(rowCount, previewRowCount) + (growsDuringDrag ? 1 : 0)
|
||||
: rowCount;
|
||||
|
||||
const { ref: droppableRef, isDropTarget } = useDroppable<GridTargetData>({
|
||||
@@ -876,6 +885,14 @@ const DndGridEntry = ({ sectionId, placement, label, columnCount, maxRowCount }:
|
||||
],
|
||||
[],
|
||||
);
|
||||
const modifiers = useMemo(
|
||||
() => [
|
||||
RestrictToElement.configure({
|
||||
element: () => shellRef.current?.closest('[data-testid="board-canvas"]') ?? null,
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
const {
|
||||
ref: draggableRef,
|
||||
handleRef,
|
||||
@@ -886,6 +903,7 @@ const DndGridEntry = ({ sectionId, placement, label, columnCount, maxRowCount }:
|
||||
type: GRID_ENTRY_TYPE,
|
||||
data: sourceData,
|
||||
sensors,
|
||||
modifiers,
|
||||
});
|
||||
const setShellRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Box, LoadingOverlay } from "@mantine/core";
|
||||
import combineClasses from "clsx";
|
||||
@@ -13,12 +13,14 @@ import type { ContainerSectionItem, Section } from "~/app/[locale]/boards/_types
|
||||
import {
|
||||
getCollapsedDisplayLayout,
|
||||
getEditableCanvasAttributes,
|
||||
getGridRowCountForVisualHeight,
|
||||
getLayoutRowCount,
|
||||
getLogicalItemStyle,
|
||||
getLogicalTrackSize,
|
||||
getReadonlyCanvasAttributes,
|
||||
normalizeGridPlacement,
|
||||
} from "~/components/board/layout";
|
||||
import { useBoardCanvasScale } from "~/components/board/layout/scaled-board-canvas";
|
||||
import { loadGridEditorAsync } from "./grid-editor-loader";
|
||||
import { useGridEditorRuntimeStatus } from "./grid-editor-runtime";
|
||||
import type { SectionGridPlacement } from "./use-grid-layout-actions";
|
||||
@@ -81,6 +83,7 @@ export const SectionGrid = ({
|
||||
className,
|
||||
}: SectionGridProps) => {
|
||||
const [isEditMode] = useEditMode();
|
||||
const canvasScale = useBoardCanvasScale();
|
||||
const editorRuntimeStatus = useGridEditorRuntimeStatus();
|
||||
const board = useRequiredBoard();
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
@@ -144,8 +147,12 @@ export const SectionGrid = ({
|
||||
);
|
||||
const displayedItems = items.map((item) => withPlacement(item, placementById.get(item.id)));
|
||||
const displayedInnerSections = innerSections.map((item) => withPlacement(item, placementById.get(item.id)));
|
||||
const minimumViewportRowCount = useMinimumViewportRowCount(
|
||||
section.kind === "empty" && railPlacement === "main",
|
||||
canvasScale,
|
||||
);
|
||||
const contentRowCount = Math.max(1, getLayoutRowCount(displayPlacements));
|
||||
const rowCount = Math.max(contentRowCount, requestedRowCount);
|
||||
const rowCount = Math.max(contentRowCount, requestedRowCount, minimumViewportRowCount);
|
||||
const logicalWidth = getLogicalTrackSize(columnCount);
|
||||
const logicalHeight = getLogicalTrackSize(rowCount);
|
||||
const canvasAttributes = isEditMode
|
||||
@@ -199,6 +206,24 @@ export const SectionGrid = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useMinimumViewportRowCount = (enabled: boolean, canvasScale: number) => {
|
||||
const [visualHeight, setVisualHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const update = () => setVisualHeight(window.visualViewport?.height ?? window.innerHeight);
|
||||
update();
|
||||
window.addEventListener("resize", update, { passive: true });
|
||||
window.visualViewport?.addEventListener("resize", update, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("resize", update);
|
||||
window.visualViewport?.removeEventListener("resize", update);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return enabled ? getGridRowCountForVisualHeight(visualHeight, canvasScale) : 0;
|
||||
};
|
||||
|
||||
const getContainerMinimumSize = (
|
||||
board: ReturnType<typeof useRequiredBoard>,
|
||||
layoutId: string,
|
||||
|
||||
+49
-30
@@ -9,7 +9,6 @@ import { describe, test } from "vitest";
|
||||
import {
|
||||
LOGICAL_GRID_CELL_SIZE,
|
||||
LOGICAL_GRID_GAP,
|
||||
MIN_ACCESSIBLE_CANVAS_SCALE,
|
||||
} from "../apps/nextjs/src/components/board/layout/constants";
|
||||
import * as sqliteSchema from "../packages/db/schema/sqlite";
|
||||
import { createHomarrContainer } from "./shared/create-homarr-container";
|
||||
@@ -26,7 +25,6 @@ const gridRuntimeMarker = "homarr-dnd-kit-v1";
|
||||
const dndKitPayloadMarker = "dnd-kit-description";
|
||||
const logicalCellSize = LOGICAL_GRID_CELL_SIZE;
|
||||
const logicalCellPitch = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP;
|
||||
const minimumCanvasScale = MIN_ACCESSIBLE_CANVAS_SCALE;
|
||||
|
||||
describe("Board grid", () => {
|
||||
test("supports fixed, nested, rail, pointer, keyboard and reconciliation workflows", async () => {
|
||||
@@ -174,21 +172,17 @@ describe("Board grid", () => {
|
||||
expect(await readLayoutShiftScoreAsync(page)).toBeLessThan(0.01);
|
||||
|
||||
// CSS zoom exercises Chromium's browser-zoom layout path without relying
|
||||
// on desktop browser chrome. At 200% the scale floor must preserve
|
||||
// magnification and expose horizontal scrolling instead of shrinking the
|
||||
// board by the inverse zoom factor.
|
||||
// on desktop browser chrome. The board must keep fitting without adding
|
||||
// horizontal scrolling at any effective zoom.
|
||||
await setDocumentZoomAsync(page, 2);
|
||||
await expect(canvas).toHaveAttribute("data-canvas-scale", String(minimumCanvasScale));
|
||||
await expect(canvas).toHaveAttribute("data-canvas-overflow", "true");
|
||||
const zoomedCanvasScale = await readCanvasScaleAsync(canvas);
|
||||
expect(zoomedCanvasScale).toBeCloseTo(normalCanvasScale / 2, 2);
|
||||
await expect(canvas).toHaveAttribute("data-canvas-overflow", "false");
|
||||
await expectFixedLogicalTileAsync(logicalTile);
|
||||
await expectUniformVisualScaleAsync(logicalTile);
|
||||
const fittedClockFontSize = await firstItem.locator(".clock-time-text").evaluate((element) => {
|
||||
return Number.parseFloat(getComputedStyle(element).fontSize);
|
||||
});
|
||||
expect(fittedClockFontSize * minimumCanvasScale).toBeGreaterThanOrEqual(25);
|
||||
const zoomedTileBox = await expectBoundingBoxAsync(logicalTile);
|
||||
expect(zoomedTileBox.width).toBeGreaterThan(normalZoomTileBox.width * 1.25);
|
||||
await expectHorizontalOverflowAsync(canvas);
|
||||
expect(zoomedTileBox.width).toBeCloseTo(normalZoomTileBox.width, 1);
|
||||
await expectNoHorizontalOverflowAsync(canvas);
|
||||
await expectDocumentNotHorizontallyScrollableAsync(page);
|
||||
const overflowRailY = (await expectBoundingBoxAsync(rail)).y;
|
||||
await page.evaluate(() => window.scrollTo({ top: 300, behavior: "auto" }));
|
||||
@@ -337,8 +331,11 @@ describe("Board grid", () => {
|
||||
await expect(itemSelectDialog).toHaveCount(0);
|
||||
|
||||
const mainGrid = page.locator(`[data-grid-section-id="${fixture.sectionId}"]`);
|
||||
const editorViewport = page.viewportSize();
|
||||
if (!editorViewport) throw new Error("Expected a browser viewport");
|
||||
expect((await expectBoundingBoxAsync(mainGrid)).height).toBeGreaterThanOrEqual(editorViewport.height - 1);
|
||||
const interactionSnapshot = await readEditorGridSnapshotAsync(mainGrid);
|
||||
await expectGridBoundaryRejectsOverflowAsync(page, mainGrid, firstItem);
|
||||
await expectGridGrowsAtBoundaryAsync(page, canvas, mainGrid, firstItem);
|
||||
await firstItem.locator("[data-grid-item-content]").evaluate((element) => {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.dataset.e2eDragIframe = "true";
|
||||
@@ -367,8 +364,8 @@ describe("Board grid", () => {
|
||||
.evaluateAll((elements) => elements.forEach((element) => element.remove()));
|
||||
|
||||
await page.setViewportSize({ width: 600, height: 1200 });
|
||||
await expect(canvas).toHaveAttribute("data-canvas-scale", String(minimumCanvasScale));
|
||||
await expectHorizontalOverflowAsync(canvas);
|
||||
await expect(canvas).toHaveAttribute("data-canvas-overflow", "false");
|
||||
await expectNoHorizontalOverflowAsync(canvas);
|
||||
await expectDocumentNotHorizontallyScrollableAsync(page);
|
||||
for (const direction of ["n", "ne", "e", "se", "s", "sw", "w", "nw"] as const) {
|
||||
await expectTargetSizeAsync(firstItem.locator(`:scope > [data-grid-resize-handle="${direction}"]`));
|
||||
@@ -718,13 +715,12 @@ describe("Board grid", () => {
|
||||
expect(continuousResizeWidth).toBeGreaterThan(logicalCellSize * canvasScale);
|
||||
|
||||
await page.mouse.move(eastResizeStart.x - logicalCellPitch * canvasScale, eastResizeStart.y, { steps: 6 });
|
||||
await expect(containerSection).toHaveAttribute("data-grid-w", "2");
|
||||
await expect(containerSection).toHaveAttribute("data-grid-w", "1");
|
||||
await expect(resizePlaceholder).toHaveAttribute("data-grid-w", "1");
|
||||
expect((await expectBoundingBoxAsync(resizeOutline)).width).toBeCloseTo(logicalCellSize * canvasScale, 1);
|
||||
const liveNestedGridBox = await expectBoundingBoxAsync(nestedEditor);
|
||||
const liveContainerBox = await expectBoundingBoxAsync(containerSection);
|
||||
expect(liveNestedGridBox.width).toBeCloseTo(originalResizeWidth, 1);
|
||||
expect(liveNestedGridBox.width).toBeLessThanOrEqual(liveContainerBox.width + 1);
|
||||
expect(liveContainerBox.width).toBeCloseTo(logicalCellSize * canvasScale, 1);
|
||||
await expect(containerSection.locator(":scope > .board-grid-content-mount")).toHaveCSS("overflow", "clip");
|
||||
await page.mouse.up();
|
||||
await expect(containerSection).toHaveAttribute("data-grid-w", "1");
|
||||
await expect(resizeOutline).toHaveCount(0);
|
||||
@@ -759,15 +755,26 @@ describe("Board grid", () => {
|
||||
await expect(containerSection).toHaveAttribute("data-grid-h", "2");
|
||||
|
||||
const southResizeHandle = containerSection.locator(':scope > [data-grid-resize-handle="s"]');
|
||||
await dragLocatorByAsync(page, southResizeHandle, 0, logicalCellPitch * canvasScale);
|
||||
const gridHeightBeforeSouthResize = (await expectBoundingBoxAsync(mainGrid)).height;
|
||||
const southResizeBox = await expectBoundingBoxAsync(southResizeHandle);
|
||||
const southResizeStart = {
|
||||
x: southResizeBox.x + southResizeBox.width / 2,
|
||||
y: southResizeBox.y + southResizeBox.height / 2,
|
||||
};
|
||||
await page.mouse.move(southResizeStart.x, southResizeStart.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(southResizeStart.x, southResizeStart.y + logicalCellPitch * canvasScale, { steps: 8 });
|
||||
await expect(containerSection).toHaveAttribute("data-grid-h", "3");
|
||||
await expect(belowItem).toHaveAttribute("data-grid-y", "3");
|
||||
await expect
|
||||
.poll(async () => (await expectBoundingBoxAsync(mainGrid)).height)
|
||||
.toBeGreaterThan(gridHeightBeforeSouthResize);
|
||||
await page.mouse.up();
|
||||
expect(
|
||||
gridLayoutsOverlap(await readGridLayoutAsync(containerSection), await readGridLayoutAsync(belowItem)),
|
||||
).toBe(false);
|
||||
|
||||
// Auto-scroll needs real document overflow. Grow the canvas through an
|
||||
// explicit resize instead of relying on edit mode to add empty rows.
|
||||
// Grow the canvas further to exercise sustained document auto-scroll.
|
||||
await dragLocatorByAsync(page, southResizeHandle, 0, logicalCellPitch * canvasScale * 6);
|
||||
await expect(containerSection).toHaveAttribute("data-grid-h", "9");
|
||||
await expect(belowItem).toHaveAttribute("data-grid-y", "9");
|
||||
@@ -1713,9 +1720,10 @@ const expectImmediateOutsideReleaseRestoresSnapshotAsync = async (page: Page, gr
|
||||
await expect.poll(async () => await readEditorGridSnapshotAsync(grid)).toEqual(snapshot);
|
||||
};
|
||||
|
||||
const expectGridBoundaryRejectsOverflowAsync = async (page: Page, grid: Locator, locator: Locator) => {
|
||||
const expectGridGrowsAtBoundaryAsync = async (page: Page, canvas: Locator, grid: Locator, locator: Locator) => {
|
||||
const snapshot = await readEditorGridSnapshotAsync(grid);
|
||||
const gridBox = await expectBoundingBoxAsync(grid);
|
||||
const canvasBox = await expectBoundingBoxAsync(canvas);
|
||||
const itemBox = await expectBoundingBoxAsync(locator);
|
||||
const start = { x: itemBox.x + itemBox.width * 0.63, y: itemBox.y + itemBox.height * 0.42 };
|
||||
|
||||
@@ -1723,11 +1731,22 @@ const expectGridBoundaryRejectsOverflowAsync = async (page: Page, grid: Locator,
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(start.x + 8, start.y);
|
||||
await expect(locator).toHaveAttribute("data-dnd-drag-source", "true");
|
||||
await page.mouse.move(gridBox.x + gridBox.width / 2, gridBox.y + gridBox.height + logicalCellPitch, { steps: 12 });
|
||||
await expect(page.locator("[data-grid-placeholder-for]")).toHaveCount(0);
|
||||
const activeGridBox = await expectBoundingBoxAsync(grid);
|
||||
expect(activeGridBox.height).toBeCloseTo(gridBox.height, 1);
|
||||
await expect
|
||||
.poll(async () => (await expectBoundingBoxAsync(grid)).height)
|
||||
.toBeGreaterThan(gridBox.height);
|
||||
|
||||
await page.mouse.move(gridBox.x + gridBox.width / 2, gridBox.y + gridBox.height + logicalCellPitch / 2, {
|
||||
steps: 12,
|
||||
});
|
||||
await expect(page.locator("[data-grid-placeholder-for]")).toHaveCount(1);
|
||||
const activeGridBox = await expectBoundingBoxAsync(grid);
|
||||
expect(activeGridBox.height).toBeGreaterThan(gridBox.height);
|
||||
|
||||
await page.mouse.move(canvasBox.x + canvasBox.width + 1000, start.y, { steps: 12 });
|
||||
const overlayBox = await expectBoundingBoxAsync(page.locator(".board-grid-drag-overlay[data-dnd-dragging]"));
|
||||
expect(overlayBox.x + overlayBox.width).toBeLessThanOrEqual(canvasBox.x + canvasBox.width + 1);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await page.mouse.up();
|
||||
await expect(page.locator("body")).not.toHaveAttribute("data-board-grid-interacting");
|
||||
await expect.poll(async () => await readEditorGridSnapshotAsync(grid)).toEqual(snapshot);
|
||||
@@ -2036,7 +2055,7 @@ const expectUniformVisualScaleAsync = async (locator: Locator) => {
|
||||
expect(scale.horizontal).toBeCloseTo(scale.vertical, 3);
|
||||
};
|
||||
|
||||
const expectHorizontalOverflowAsync = async (locator: Locator) => {
|
||||
const expectNoHorizontalOverflowAsync = async (locator: Locator) => {
|
||||
const dimensions = await locator.evaluate((element) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
return {
|
||||
@@ -2045,7 +2064,7 @@ const expectHorizontalOverflowAsync = async (locator: Locator) => {
|
||||
};
|
||||
});
|
||||
|
||||
expect(dimensions.scrollWidth).toBeGreaterThan(dimensions.clientWidth);
|
||||
expect(dimensions.scrollWidth - dimensions.clientWidth).toBeLessThanOrEqual(1);
|
||||
};
|
||||
|
||||
const expectDocumentNotHorizontallyScrollableAsync = async (page: Page) => {
|
||||
|
||||
Reference in new issue
Block a user