Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/drag-target-switch-margin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"nextly": patch
"create-nextly-app": patch
"@nextlyhq/admin": patch
"@nextlyhq/admin-css": patch
"@nextlyhq/blocks-engine": patch
"@nextlyhq/blocks-react": patch
"@nextlyhq/ui": patch
"@nextlyhq/adapter-drizzle": patch
"@nextlyhq/adapter-postgres": patch
"@nextlyhq/adapter-mysql": patch
"@nextlyhq/adapter-sqlite": patch
"@nextlyhq/storage-s3": patch
"@nextlyhq/storage-uploadthing": patch
"@nextlyhq/storage-vercel-blob": patch
"@nextlyhq/plugin-form-builder": patch
"@nextlyhq/plugin-page-builder": patch
"@nextlyhq/plugin-seo": patch
"@nextlyhq/plugin-sdk": patch
"@nextlyhq/eslint-config": patch
"@nextlyhq/prettier-config": patch
"@nextlyhq/telemetry": patch
"@nextlyhq/tsconfig": patch
"@nextlyhq/builder": patch
"@nextlyhq/module-specifiers": patch
---

Hold the drop target across a switch margin while dragging on the page-builder canvas, so a pointer resting near a zone boundary no longer flips between insertion points on every small movement.
13 changes: 1 addition & 12 deletions e2e/tests/canvas/acceptance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,7 @@ test.describe("a canvas any Nextly editor could ship", () => {
test("holds its target through a jitter at a zone boundary", async ({
request,
}) => {
note(
PLAN_POINT.targetSwitchHysteresis,
"B-7",
"this canvas has no switch margin: bracketed at an edge, the target " +
"flips on every 2px crossing"
);
note(PLAN_POINT.targetSwitchHysteresis, "B-7");
await driver.mountTree(await seedPage(request, FLAT_LIST_FIXTURE));
// Onto a zone, then to that zone's EDGE. Both halves are preconditions with
// teeth. Jittering from dead space counts the indicator appearing and
Expand Down Expand Up @@ -546,12 +541,6 @@ test.describe("a canvas any Nextly editor could ship", () => {
"the indicator must be visible to measure whether it moves"
).toBeGreaterThanOrEqual(0);

// Marked only now. Everything above ran unprotected, so a failed seed, a
// drag that never reached a zone, a target that never moved, or a probe the
// runner was too slow to take stays a real outcome of its own rather than
// becoming another expected failure.
test.fail(true, "the target flips on every 2px crossing of a zone edge");

// The log's FIRST entry is the state when recording began, not a change, so
// anything after it is motion the jitter caused. Comparing the whole log
// instead asserts an array that can never be empty, which would keep this
Expand Down
29 changes: 15 additions & 14 deletions e2e/tests/canvas/scenarios.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,23 @@ test("scenario 4: a steady drag over variable-height blocks never reverses", asy
});

/**
* Expected failure: this canvas has no target-switch hysteresis.
* The target is sticky across a margin, so a 2px jitter cannot move it.
*
* The requirement is a sticky target with an 8-12px dead-zone margin or a
* >100ms dwell, so that oscillating the pointer 2px around any boundary never
* flips the indicator. Observed here, with the edge bracketed to 1px and the
* samples taken on opposite sides of it: `[1,2,1,2,...]`, a flip on every move.
* The requirement is an 8-12px dead-zone margin or a >100ms dwell, so that
* oscillating the pointer 2px around any boundary never flips the indicator.
* The canvas satisfies the distance half: a zone holds the target until a
* challenger is nearer by the full margin, which no 2px move can achieve.
*
* Both halves of the method are load-bearing. Jittering anywhere other than a
* bracketed edge measures the middle of one zone's catchment, and sampling P
* and P+2 rather than P-2 and P+2 keeps both samples on the same side; either
* reports a stable indicator on a canvas that has none.
* Both halves of the method are load-bearing, and they are what make a green
* run mean anything. Jittering anywhere other than a bracketed edge measures
* the middle of one zone's catchment, and sampling P and P+2 rather than P-2
* and P+2 keeps both samples on the same side; either one reports a stable
* indicator whether or not the margin exists, so the test would keep passing if
* the margin were removed.
*
* The fixture alternates 400px and 24px siblings deliberately: the 24px pitch
* is the narrowest spacing the margin has to stay inside, since a margin wider
* than the spacing would carry the target past the next zone's centre.
*/
test("scenario 4b: a 2px jitter at a zone edge keeps the indicator stable", async ({
page,
Expand Down Expand Up @@ -434,11 +440,6 @@ test("scenario 4b: a 2px jitter at a zone edge keeps the indicator stable", asyn
`the drag must find a target at all: ${JSON.stringify(observed)}`
).toBe(true);

test.fail(
true,
"no target-switch hysteresis: the indicator flips on every 2px move"
);

// The log's first entry is the state when recording began, so anything after
// it is a change the jitter caused. -1 is NOT excluded: an indicator that
// vanishes and returns is the same defect seen from the other side, and it
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-page-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"test:integration": "vitest run --config vitest.integration.config.ts"
},
"dependencies": {
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/collision": "0.5.0",
"@dnd-kit/dom": "0.5.0",
"@dnd-kit/react": "0.5.0",
"@nextlyhq/blocks-engine": "workspace:*",
Expand All @@ -74,7 +76,6 @@
"react-hook-form": ">=7.0.0"
},
"devDependencies": {
"@dnd-kit/abstract": "0.5.0",
"@nextlyhq/admin": "workspace:*",
"@nextlyhq/plugin-sdk": "workspace:*",
"@nextlyhq/tsconfig": "workspace:*",
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-page-builder/src/admin/canvas/DropZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import { useDragDropMonitor, useDroppable } from "@dnd-kit/react";
import { createContext, useContext, useState, type ReactNode } from "react";

import { insertionCollisionDetector } from "./collisionPolicy";

const BLOCK_TYPE = "nx-block";

/**
Expand Down Expand Up @@ -123,6 +125,14 @@ export function DropZone({
// is the only thing that can settle two IDENTICAL rectangles — which is
// exactly what a nested container's edge gap and its parent's gap are.
collisionPriority: canvasPriority(depth),
// Only the zones interleaved between a slot's children. They all span the
// same container, so they share a width and an axis — which is what makes a
// margin measured on one axis a constant physical width for them, and makes
// "the pointer is inside this zone" and "this zone's centre is nearest" the
// same statement. Targets that hold neither property, including this
// component's own empty placeholder, keep the default ranking until a
// detector exists that resolves a REGION before it measures a distance.
collisionDetector: empty ? undefined : insertionCollisionDetector,
Comment thread
mobeenabdullah marked this conversation as resolved.
});

if (empty) {
Expand Down
Loading
Loading