Skip to content

feat(playground): draw drop indicators for container blocks#2851

Draft
christianhg wants to merge 1 commit into
mainfrom
container-drop-indicators
Draft

feat(playground): draw drop indicators for container blocks#2851
christianhg wants to merge 1 commit into
mainfrom
container-drop-indicators

Conversation

@christianhg

Copy link
Copy Markdown
Member

Dragging a callout, fact-box, or table around the playground gave no hint of where it would land. Move it over a paragraph and a thin line shows the drop edge; move it over another container and there is nothing. The drag still works, you just can't see the target.

The line is engine chrome that the new render pipeline opts out of. For legacy top-level blocks the engine tracks a drop position during the drag and render.text-block.tsx draws a 1px DropIndicator for it via useElementDropPosition. Blocks rendered through the defineX pipeline (every container and its nested text blocks) render no indicator on purpose: where a dragged block lands is pointer-driven UI, not document structure, so the engine leaves it to the consumer.

@portabletext/plugin-dnd is that consumer hook. Its useDropPosition(path) exposes the same position the engine computes, derived from the public drag.* behavior events, as 'start' | 'end' | undefined. This mounts DndProvider inside EditorProvider (alongside the existing ListIndexProvider) and adds a shared BlockDropIndicator that reads the position for its path and draws the line: 1px currentColor at the block's top edge for a 'start' drop, the bottom for 'end', matching the engine's pt-drop-indicator so container and top-level indicators look identical. It is a <span> so it stays valid markup inside a <p> text block, and position: absolute so it never reflows the text; the block element it renders into is relative.

function BlockDropIndicator({path}: {path: Path}) {
  const position = useDropPosition(path)
  if (position === undefined) return null
  return <span style={{position: 'absolute', /* top or bottom: 0 */ borderTop: '1px solid currentColor'}} />
}

Putting that on every block type without copying it drove a consolidation. The callout, fact-box, and cell text-block renders each repeated the same shape: a list-item branch (re-emitting the data-list-* attributes the counter CSS keys off) and a switch (node.style) returning a styled element, differing only in which styles each container allows. That collapses into one ContainerTextBlock parameterized by a {style: {tag, className}} map, which subsumes the former ListItemBlock (the data-list-* re-emission moves into it unchanged) and renders the indicator in one place. The three top-level container elements render BlockDropIndicator directly.

List rendering output is unchanged: the same data-list-* attributes on the same wrapper, the same element and classNames per style, now with position: relative added (no offsets, so no layout shift) and the indicator as a trailing child. Row and cell <tr>/<td> get no direct indicator yet: table-internal absolute positioning is fragile and neither is drag-initiated, so their content blocks carry the coverage. The position tracking itself is pinned in the plugin's own plugin.dnd.test.tsx; the playground side is render glue, so it leans on that rather than restating it.

During a block drag the engine tracks a drop position and the legacy
top-level renders draw a 1px line for it (`useElementDropPosition` ->
`DropIndicator`). The new `defineX` pipeline renders no such chrome by
design, so the playground's containers (callout, fact-box, table) and
their nested text blocks gave no feedback about where a dragged block
would land: dragging a callout over another container showed nothing.

Wire `@portabletext/plugin-dnd`, whose `useDropPosition(path)` exposes the
same position the engine computes, off the public `drag.*` events. Mount
`DndProvider` inside `EditorProvider` and add a shared
`BlockDropIndicator` that reads the position for its path and draws a 1px
`currentColor` line at the block's top edge for a `'start'` drop, the
bottom edge for `'end'`, mirroring the engine's `pt-drop-indicator`. It is
a `<span>` so it stays valid inside a `<p>` text block, and `position:
absolute` so it never shifts the text; the block element it renders into
is `relative`.

To put the indicator on every block type without copying it, the per-
container text-block render is consolidated: callout, fact-box, and cell
each repeated the same list-item / style-switch / (now) indicator logic,
differing only in which styles they allow. That collapses into one
`ContainerTextBlock` parameterized by a style map, which subsumes the
former `ListItemBlock` (the `data-list-*` re-emission moves into it
unchanged). The three top-level container elements render the indicator
directly.

List rendering output is unchanged: the same `data-list-*` attributes on
the same wrapper, the same elements and classNames per style. Row and cell
`<tr>`/`<td>` get no direct indicator yet (table-internal absolute
positioning is fragile and neither is drag-initiated); their content
blocks are covered.
@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fa918d4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-example-basic Ready Ready Preview, Comment Jun 24, 2026 11:33am
portable-text-playground Ready Ready Preview, Comment Jun 24, 2026 11:33am

Request Review

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (946e5bb2)

@portabletext/editor

Metric Value vs main (946e5bb)
Internal (raw) 788.2 KB -
Internal (gzip) 150.6 KB -
Bundled (raw) 1.40 MB -
Bundled (gzip) 314.6 KB -
Import time 97ms -1ms, -0.7%

@portabletext/editor/behaviors

Metric Value vs main (946e5bb)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -1.7%

@portabletext/editor/plugins

Metric Value vs main (946e5bb)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms +0ms, +1.2%

@portabletext/editor/selectors

Metric Value vs main (946e5bb)
Internal (raw) 79.3 KB -
Internal (gzip) 14.5 KB -
Bundled (raw) 74.7 KB -
Bundled (gzip) 13.4 KB -
Import time 8ms -0ms, -0.3%

@portabletext/editor/traversal

Metric Value vs main (946e5bb)
Internal (raw) 25.1 KB -
Internal (gzip) 5.0 KB -
Bundled (raw) 25.0 KB -
Bundled (gzip) 4.9 KB -
Import time 6ms -0ms, -0.9%

@portabletext/editor/utils

Metric Value vs main (946e5bb)
Internal (raw) 29.3 KB -
Internal (gzip) 6.1 KB -
Bundled (raw) 26.8 KB -
Bundled (gzip) 5.8 KB -
Import time 6ms -0ms, -2.2%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @portabletext/markdown

Compared against main (946e5bb2)

Metric Value vs main (946e5bb)
Internal (raw) 53.0 KB -
Internal (gzip) 9.6 KB -
Bundled (raw) 348.2 KB -
Bundled (gzip) 96.1 KB -
Import time 38ms -2ms, -6.1%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant