Skip to content

feat: Portable Text-native node traversal#2398

Draft
christianhg wants to merge 17 commits intomainfrom
feat-node-traversal
Draft

feat: Portable Text-native node traversal#2398
christianhg wants to merge 17 commits intomainfrom
feat-node-traversal

Conversation

@christianhg
Copy link
Member

@christianhg christianhg commented Mar 19, 2026

The vendored Slate layer inherited a node traversal system that hardcodes .children as the only field that can contain child nodes. This works for flat content and even nested structures, but not for containers where children live in schema-defined fields like rows, cells, or content. This PR replaces that system with src/node-traversal/, a set of functions that use the schema to resolve children on any node type.

The new traversal handles three cases uniformly: editor root nodes and text blocks (use .children), container objects (look up the schema-defined child field via resolveChildArrayField), and everything else (leaf nodes, no children). All seven functions share a consistent signature taking a context object with schema and editable types, a root node, and an indexed path: getChildren, getNode, getNodes, getFirst, getLast, getParent, hasNode.

Container traversal is gated by editableTypes, a Set<string> on the editor that lists which object types have editable content (e.g. 'table', 'table.row', 'table.row.cell'). When traversal encounters an object node, it checks editableTypes before descending into its child field. This set is currently always empty, meaning containers are traversed as leaf nodes today. A future PR will populate it based on registered renderers, enabling container editing without changing the traversal layer.

The old slate/node/ helpers (getChild, getChildren, getFirst, getLast, getLeaf, getLevels, getNodeIf, getNode, getNodes, getSpanNode, getString, getTextBlockNode, getTexts, hasNode, isLeaf) and slate/editor/ wrappers (node, leaf, hasPath, getObjectNode) are inlined at their call sites and deleted. The higher-level functions above, nodes, and levels remain since they add location resolution and match filtering that callers depend on, but they now delegate to the new traversal internally.

Note

All traversal functions return undefined on failure instead of throwing. This is a deliberate shift from Slate's original philosophy of throwing on every invalid path. The editor runs in a racy DOM environment where transient path mismatches are normal during React re-renders, not bugs that should crash the component tree.

@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

⚠️ No Changeset found

Latest commit: a898728

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
Copy link

vercel bot commented Mar 19, 2026

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

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Mar 20, 2026 2:22pm
portable-text-example-basic Ready Ready Preview, Comment Mar 20, 2026 2:22pm
portable-text-playground Ready Ready Preview, Comment Mar 20, 2026 2:22pm

Request Review

@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (055bdb16)

@portabletext/editor

Metric Value vs main (055bdb1)
Internal (raw) 771.7 KB +16.7 KB, +2.2%
Internal (gzip) 144.1 KB +2.2 KB, +1.5%
Bundled (raw) 1.38 MB +16.7 KB, +1.2%
Bundled (gzip) 306.9 KB +2.1 KB, +0.7%
Import time 106ms -0ms, -0.4%

@portabletext/editor/behaviors

Metric Value vs main (055bdb1)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 6ms -0ms, -4.8%

@portabletext/editor/plugins

Metric Value vs main (055bdb1)
Internal (raw) 2.5 KB -
Internal (gzip) 910 B -
Bundled (raw) 2.3 KB -
Bundled (gzip) 839 B -
Import time 12ms -1ms, -5.9%

@portabletext/editor/selectors

Metric Value vs main (055bdb1)
Internal (raw) 60.2 KB -
Internal (gzip) 9.4 KB -
Bundled (raw) 56.7 KB -
Bundled (gzip) 8.6 KB -
Import time 11ms -0ms, -2.2%

@portabletext/editor/utils

Metric Value vs main (055bdb1)
Internal (raw) 24.2 KB -
Internal (gzip) 4.7 KB -
Bundled (raw) 22.2 KB -
Bundled (gzip) 4.4 KB -
Import time 10ms -0ms, -2.3%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./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.

@christianhg christianhg force-pushed the feat-node-traversal branch from e1709db to 54eedf1 Compare March 19, 2026 16:31
@christianhg christianhg force-pushed the feat-node-traversal branch from 54eedf1 to cfdcf10 Compare March 19, 2026 17:07
@christianhg christianhg force-pushed the feat-simpler-dom-mapping branch 5 times, most recently from a16d425 to 285fe40 Compare March 20, 2026 08:16
@christianhg christianhg force-pushed the feat-node-traversal branch from cfdcf10 to d648f4d Compare March 20, 2026 08:58
@christianhg christianhg force-pushed the feat-node-traversal branch from 88a9829 to d8e304e Compare March 20, 2026 09:18
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