Skip to content

Blocks V3: keep the caret in inline text editing when the preview re-renders - #575

Open
wojtekpiskorz wants to merge 1 commit into
WordPress:trunkfrom
wojtekpiskorz:fix/inline-editing-caret
Open

wojtekpiskorz wants to merge 1 commit into
WordPress:trunkfrom
wojtekpiskorz:fix/inline-editing-caret

Conversation

@wojtekpiskorz

Copy link
Copy Markdown

When you type into an inline editable field (acf_inline_text_editing_attrs()), the block fetches its preview again. When that preview renders, the caret jumps to the start of the field, so the next keystrokes land there: typing AAA, pausing, then typing BBB at the end of Accessible gives BBBAccessibleAAA.

Cause. The browser edits the contentEditable text itself, so React still holds the text of the previous preview. When the new preview commits, React writes the new text into the text node, even though the DOM already shows it. Setting a text node's nodeValue moves any selection inside it to offset 0 (DOM "replace data"). The full analysis is in the issue.

What changed

  • New PreserveContentEditableSelection component (assets/src/js/pro/blocks-v3/components/preserve-content-editable-selection.js) wraps the parsed preview in BlockEditInner.
    • Before React updates the DOM (getSnapshotBeforeUpdate()), it reads the selection as character offsets, but only when focus is on an inline editable element ([data-acf-inline-contenteditable]) inside this block.
    • After the update (componentDidUpdate()), it puts the selection back if that element still has focus.
  • Anchor and focus are both kept, so a selected range keeps its extent and direction.
  • If the server renders shorter text, the caret goes to the end of the text.
  • Nothing changes when focus is elsewhere, or when React replaced the element. Other blocks are not touched.

It is a class component because getSnapshotBeforeUpdate() is the only way to read the DOM after React renders but before it writes; hooks have no equivalent.

I considered keeping React from rewriting the focused element, and rejected it. React rewrites any text it manages, so this would mean taking the element's text out of React's control. While it has focus, the element would then not show what the server renders, for example a value a render template transforms (such as wptexturize() curling quotes).

How to test

  1. Activate tests/e2e/plugins/scf-test-inline-editing-block.php and add the "SCF Inline Editing Block" to a post.
  2. Click at the end of the title, type AAA, wait about a second, type BBB. Before this change: BBBHelloAAA. After: HelloAAABBB.
  3. Do the same in the middle of the subtitle, and with a second copy of the block in the post. The caret stays where you type.

Tests

  • Jest (tests/js/blocks-v3/block-edit.test.js, "inline text editing"): three cases (end of text, middle of text, server text shorter than typed). All three fail on trunk with the caret at offset 0 and pass with this change. Full suite: 38 suites, 962 tests pass.
  • E2E (tests/e2e/block-v3-inline-editing.spec.ts, new fixture block scf/inline-editing-block): typing at the end of one field, and in the middle of another. On trunk they fail with BBBHelloAAA and YWoXrld. With this change they pass on WordPress 7.1.3 (five runs in a row), 6.9 and trunk (7.2-beta1). The full E2E suite passes on 7.1.3 (260 tests); only Chromium was run.

The Blocks V3 sources are a readable port of ACF PRO's code, so a later ACF backport that replaces block-edit.js has to keep the wrapper.

Closes #574

The Blocks V3 code is based on ACF PRO 6.7.0.2, and the ACF changelog up to 6.8.10 lists no fix, so ACF PRO probably has the same bug. Not checked against ACF PRO itself.

Use of AI Tools

This PR was written with Claude Code (Claude Opus 5.5): the root-cause analysis, code, tests and this description. I reviewed the changes and take responsibility for them.

🤖 Generated with Claude Code

…renders

Typing into an inline editable field fetches the block preview again.
React still holds the previous preview's text for the contentEditable
element, so the commit writes the new text into its text node, and
setting nodeValue moves the selection inside it to offset 0: the next
keystrokes land at the start of the field.

Wrap the parsed preview in PreserveContentEditableSelection, which reads
the selection of the focused inline editable element of the block as
character offsets in getSnapshotBeforeUpdate() and restores it in
componentDidUpdate().

Adds Jest tests in block-edit.test.js and an E2E test with a fixture
block using acf_inline_text_editing_attrs().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @wojtekpiskorz.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

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.

Blocks V3 inline text editing: the caret jumps to the start of the field after the preview re-renders

1 participant