{
+const DndGridEntry = ({ sectionId, placement, label, columnCount, maxRowCount }: DndGridEntryProps) => {
const { acquireContainer } = useBoardGridPortalHost();
const { interaction, beginResize, previewResize, completeResize, cancelResize } = useBoardGridEditor();
const [isSectionChromeActive, setIsSectionChromeActive] = useState(false);
+ const [continuousResize, setContinuousResize] = useState<{
+ placement: SectionGridPlacement;
+ valid: boolean;
+ } | null>(null);
const mountRef = useRef
(null);
const shellRef = useRef(null);
const sourceData = useMemo(
@@ -932,6 +939,14 @@ const DndGridEntry = ({ sectionId, placement, label }: DndGridEntryProps) => {
const isActive = interaction?.activeId === placement.id;
const style = getLogicalItemStyle(placement) as CSSProperties;
+ const continuousResizeStyle =
+ continuousResize && isActive
+ ? getLogicalItemStyle({
+ ...continuousResize.placement,
+ x: continuousResize.placement.x - placement.x,
+ y: continuousResize.placement.y - placement.y,
+ })
+ : undefined;
return (
{
data-dnd-dropping={isDropping ? "true" : undefined}
>
+ {continuousResizeStyle && (
+
+ )}
);
@@ -969,10 +996,13 @@ interface GridResizeHandlesProps {
placement: SectionGridPlacement;
label: string;
disabled: boolean;
+ columnCount: number;
+ maxRowCount: number | null;
beginResize: BoardGridEditorContextValue["beginResize"];
previewResize: BoardGridEditorContextValue["previewResize"];
completeResize: BoardGridEditorContextValue["completeResize"];
cancelResize: BoardGridEditorContextValue["cancelResize"];
+ setContinuousResize: (preview: { placement: SectionGridPlacement; valid: boolean } | null) => void;
}
const RESIZE_DIRECTIONS: readonly GridResizeDirection[] = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
@@ -982,10 +1012,13 @@ const GridResizeHandles = ({
placement,
label,
disabled,
+ columnCount,
+ maxRowCount,
beginResize,
previewResize,
completeResize,
cancelResize,
+ setContinuousResize,
}: GridResizeHandlesProps) => {
const pointerStartRef = useRef<{
id: number;
@@ -1033,18 +1066,35 @@ const GridResizeHandles = ({
const started = pointerStartRef.current;
if (!started || started.id !== event.pointerId) return;
event.preventDefault();
- const delta = getSnappedGridDelta({
+ const input = {
initial: { x: started.x, y: started.y },
current: { x: event.clientX, y: event.clientY },
visualScale: started.visualScale,
+ };
+ const snappedDelta = getSnappedGridDelta(input);
+ const continuousDelta = getContinuousGridDelta(input);
+ if (!snappedDelta || !continuousDelta) return;
+ const valid = update(started.direction, snappedDelta.x, snappedDelta.y);
+ setContinuousResize({
+ placement: getContinuousResizePlacement({
+ placement,
+ direction: started.direction,
+ deltaColumns: continuousDelta.x,
+ deltaRows: continuousDelta.y,
+ columnCount,
+ maxRowCount,
+ minWidth: placement.minW ?? 1,
+ minHeight: placement.minH ?? 1,
+ }),
+ valid,
});
- if (delta) update(started.direction, delta.x, delta.y);
};
const handlePointerEnd = (event: ReactPointerEvent, canceled: boolean) => {
const started = pointerStartRef.current;
if (!started || started.id !== event.pointerId) return;
pointerStartRef.current = null;
+ setContinuousResize(null);
if (event.currentTarget.hasPointerCapture(event.pointerId))
event.currentTarget.releasePointerCapture(event.pointerId);
if (canceled) cancelResize();
@@ -1073,6 +1123,7 @@ const GridResizeHandles = ({
onLostPointerCapture={(event) => {
if (pointerStartRef.current?.id !== event.pointerId) return;
pointerStartRef.current = null;
+ setContinuousResize(null);
cancelResize();
}}
/>
diff --git a/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx b/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx
index c2146ea50..8f23db2d1 100644
--- a/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx
+++ b/apps/nextjs/src/components/board/sections/grid/grid-portal-host.tsx
@@ -14,7 +14,6 @@ import { getBoardLaneColumnCount } from "~/components/board/layout";
import { SectionContentItem } from "../content";
import { SectionProvider } from "../section-context";
import { useSectionItems } from "../use-section-items";
-import { useGridResizePreview } from "./grid-editor-runtime";
import classes from "./section-grid.module.css";
interface GridPortalHostContextValue {
@@ -138,15 +137,12 @@ const GridPortalContent = ({ itemId, ownerSectionId, integrations, announce }: G
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
const { items, innerSections } = useSectionItems(ownerSectionId);
- const entryPreview = useGridResizePreview(itemId);
- const ownerPreview = useGridResizePreview(ownerSectionId);
const persistedEntry = [...items, ...innerSections].find((candidate) => candidate.id === itemId);
- const entry = withResizePreview(persistedEntry, entryPreview);
+ const entry = persistedEntry;
const rawSection = board.sections.find((section) => section.id === ownerSectionId);
const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId);
const persistedSection = toGridSection(rawSection, currentLayoutId);
- const section =
- persistedSection?.kind === "container" ? withResizePreview(persistedSection, ownerPreview) : persistedSection;
+ const section = persistedSection;
if (!entry || !section || !currentLayout) return null;
@@ -181,22 +177,6 @@ const GridPortalContent = ({ itemId, ownerSectionId, integrations, announce }: G
);
};
-const withResizePreview = <
- TValue extends { xOffset: number; yOffset: number; width: number; height: number } | null | undefined,
->(
- value: TValue,
- preview: { x: number; y: number; w: number; h: number } | null,
-): TValue =>
- value && preview
- ? ({
- ...value,
- xOffset: preview.x,
- yOffset: preview.y,
- width: preview.w,
- height: preview.h,
- } as TValue)
- : value;
-
const toGridSection = (
section: Section | undefined,
layoutId: string,
diff --git a/apps/nextjs/src/components/board/sections/item.module.css b/apps/nextjs/src/components/board/sections/item.module.css
index aa814566a..de643a756 100644
--- a/apps/nextjs/src/components/board/sections/item.module.css
+++ b/apps/nextjs/src/components/board/sections/item.module.css
@@ -21,3 +21,23 @@
border-color: transparent !important;
box-shadow: none !important;
}
+
+.containerToggle {
+ z-index: 9;
+ overflow: hidden;
+ color: var(--mantine-color-text);
+ background-color: var(--background-color);
+ border-color: var(--border-color);
+}
+
+.containerToggle :global(.mantine-Button-inner) {
+ width: 100%;
+ min-width: 0;
+}
+
+.containerToggle :global(.mantine-Button-label) {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/e2e/board-grid.spec.ts b/e2e/board-grid.spec.ts
index 44a68793e..199edfe6b 100644
--- a/e2e/board-grid.spec.ts
+++ b/e2e/board-grid.spec.ts
@@ -104,6 +104,10 @@ describe("Board grid", () => {
await expect(containerSection).toHaveAttribute("data-grid-h", "2");
const containerLabel = containerSection.locator("[data-board-container-label]");
await expect(containerLabel).toHaveText("Nested box");
+ const expandedContainerBox = await expectBoundingBoxAsync(containerSection);
+ const expandedContainerLabelBox = await expectBoundingBoxAsync(containerLabel);
+ expect(Math.abs(expandedContainerLabelBox.x - expandedContainerBox.x)).toBeLessThanOrEqual(3);
+ expect(Math.abs(expandedContainerLabelBox.width - expandedContainerBox.width)).toBeLessThanOrEqual(3);
await expect(nestedItem).toHaveAttribute("data-grid-x", "0");
await expect(belowItem).toHaveAttribute("data-grid-y", "2");
const containerLabelBox = await expectBoundingBoxAsync(containerLabel);
@@ -193,6 +197,12 @@ describe("Board grid", () => {
await expect
.poll(async () => (await expectBoundingBoxAsync(containerSection)).height)
.toBeCloseTo(expectedCollapsedHeight, 1);
+ const collapsedContainerBox = await expectBoundingBoxAsync(containerSection);
+ const collapsedControlBox = await expectBoundingBoxAsync(collapsedControl);
+ expect(Math.abs(collapsedControlBox.x - collapsedContainerBox.x)).toBeLessThanOrEqual(3);
+ expect(Math.abs(collapsedControlBox.y - collapsedContainerBox.y)).toBeLessThanOrEqual(3);
+ expect(Math.abs(collapsedControlBox.width - collapsedContainerBox.width)).toBeLessThanOrEqual(3);
+ expect(Math.abs(collapsedControlBox.height - collapsedContainerBox.height)).toBeLessThanOrEqual(3);
await page
.getByRole("button", {
@@ -637,13 +647,34 @@ describe("Board grid", () => {
};
await page.mouse.move(eastResizeStart.x, eastResizeStart.y);
await page.mouse.down();
- await page.mouse.move(eastResizeStart.x - logicalCellPitch * canvasScale, eastResizeStart.y, { steps: 12 });
- await expect(containerSection).toHaveAttribute("data-grid-w", "1");
+ await page.mouse.move(eastResizeStart.x - logicalCellPitch * canvasScale * 0.35, eastResizeStart.y, {
+ steps: 6,
+ });
+ const resizeOutline = containerSection.locator(":scope > [data-grid-resize-outline]");
+ const resizePlaceholder = mainGrid.locator(`[data-grid-placeholder-for="${fixture.containerSectionId}"]`);
+ await expect(resizeOutline).toBeVisible();
+ await expect(resizeOutline).toHaveAttribute("data-grid-resize-valid", "true");
+ await expect(resizePlaceholder).toHaveAttribute("data-grid-placeholder-mode", "resize");
+ await expect(resizePlaceholder).toHaveAttribute("data-grid-w", "2");
+ await expect(containerSection).toHaveAttribute("data-grid-w", "2");
+ const originalResizeWidth = (await expectBoundingBoxAsync(containerSection)).width;
+ const continuousResizeWidth = (await expectBoundingBoxAsync(resizeOutline)).width;
+ expect(continuousResizeWidth).toBeLessThan(originalResizeWidth);
+ 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(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(logicalCellSize * canvasScale, 1);
+ expect(liveNestedGridBox.width).toBeCloseTo(originalResizeWidth, 1);
expect(liveNestedGridBox.width).toBeLessThanOrEqual(liveContainerBox.width + 1);
await page.mouse.up();
+ await expect(containerSection).toHaveAttribute("data-grid-w", "1");
+ await expect(resizeOutline).toHaveCount(0);
+ await expect(containerSection).toHaveAttribute("data-grid-section-chrome-active", "true");
+ await expect(eastResizeHandle).not.toHaveAttribute("data-grid-resize-disabled");
await resizeLocatorByImmediateReleaseAsync(page, eastResizeHandle, logicalCellPitch * canvasScale, 0);
await expect(containerSection).toHaveAttribute("data-grid-w", "2");