Skip to content

feat: every container around the selection is focused#2813

Open
christianhg wants to merge 1 commit into
mainfrom
feat/focused-ancestors-rule
Open

feat: every container around the selection is focused#2813
christianhg wants to merge 1 commit into
mainfrom
feat/focused-ancestors-rule

Conversation

@christianhg

Copy link
Copy Markdown
Member

A container's focused prop in a defineContainer render callback now reports true for every container ancestor that fully contains the current selection, matching the symmetry selected has had since v7.

Previously the focused-container loop in get-selection-state stopped at the first hit — only the innermost container reported focused: true. An outer container chrome (the table around a focused cell, the code block around a focused line) was silently saying it wasn't focused even though the caret was inside it. Consumers wiring container chrome to focused state either accepted the lie or rebuilt the ancestor walk themselves.

The rule

A container reports focused: true when the entire current selection is contained within it.

For a collapsed caret, every container ancestor satisfies this — the caret is a point, every ancestor contains that point, every ancestor reports focused.

For an expanded selection, only containers whose subtree contains both endpoints satisfy this. Computed as the intersection of the anchor's and focus's container ancestors — a container holds both endpoints iff it is an ancestor of both.

Behaviour delta

Scenario Before After
Caret in a code-block line code block: false, line: true code block: true, line: true
Caret in a table cell paragraph table/row/cell: false, paragraph: true all four: true
Selection across two cells in one row nothing focused row + table focused; cells not
Selection across two rows nothing focused table focused; rows + cells not
Selection across two top-level paragraphs nothing focused unchanged — no common container

No existing true becomes false. All deltas are false → true for legitimate "the user is editing inside me" cases.

Implementation

SelectionState.focusedContainerPath: string | undefined becomes focusedContainerPaths: Set<string>, mirroring selectedContainerPaths. useIsFocusedContainer keeps its signature and contract (subscribe to "is this container holding the focus"); the implementation swaps the scalar equality check for .has(serializedPath). The store-and-subscribe shape in selection-state-context.tsx is unchanged — same per-slice useSyncExternalStore mechanism, same re-render surface.

The collapsed branch walks ancestors once. The expanded branch walks anchor ancestors and focus ancestors and intersects — no new traversal util needed.

Tests

  • src/editor/get-selection-state.test.ts — covers every case in the matrix above plus backward selection. Each test asserts the FULL focusedContainerPaths set via toEqual(new Set([...])), not spot checks.
  • tests/container-render-focused-selected.test.tsx — updates the existing browser tests where they encoded the innermost-only rule. The expanded-across-cells test keeps its expectations: cells still report focused: false because neither cell contains both endpoints.

Targeted browser sweep: all container suites green (64/64).

Changes the focused-container rule in get-selection-state so every container ancestor that fully contains the current selection reports focused, matching the cumulative shape selected has had since v7. Only the innermost container reported focused before; outer chrome (table around a focused cell, code block around a focused line) was silently lying.
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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 Jun 18, 2026 10:00am
portable-text-example-basic Ready Ready Preview, Comment Jun 18, 2026 10:00am
portable-text-playground Ready Ready Preview, Comment Jun 18, 2026 10:00am

Request Review

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ecde866

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@portabletext/editor Minor
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-dnd Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-list-index Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-table Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (25ad79a7)

@portabletext/editor

Metric Value vs main (25ad79a)
Internal (raw) 787.6 KB +1.1 KB, +0.1%
Internal (gzip) 150.2 KB +69 B, +0.0%
Bundled (raw) 1.39 MB +1.1 KB, +0.1%
Bundled (gzip) 312.2 KB +70 B, +0.0%
Import time 99ms -1ms, -1.4%

@portabletext/editor/behaviors

Metric Value vs main (25ad79a)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -7.1%

@portabletext/editor/plugins

Metric Value vs main (25ad79a)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms -0ms, -0.8%

@portabletext/editor/selectors

Metric Value vs main (25ad79a)
Internal (raw) 79.0 KB -
Internal (gzip) 14.5 KB -
Bundled (raw) 74.5 KB -
Bundled (gzip) 13.3 KB -
Import time 8ms -0ms, -2.8%

@portabletext/editor/traversal

Metric Value vs main (25ad79a)
Internal (raw) 25.1 KB -
Internal (gzip) 5.0 KB -
Bundled (raw) 25.0 KB -
Bundled (gzip) 4.9 KB -
Import time 6ms +0ms, +1.7%

@portabletext/editor/utils

Metric Value vs main (25ad79a)
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.0%

🗺️ . · ./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 (25ad79a7)

Metric Value vs main (25ad79a)
Internal (raw) 53.0 KB -
Internal (gzip) 9.6 KB -
Bundled (raw) 347.6 KB -
Bundled (gzip) 96.0 KB -
Import time 40ms +0ms, +0.4%

🗺️ 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