Repository navigation
[lexical-playground] Bug Fix: Share history between the image caption and its parent editor - #9566
Merged
Merged
Conversation
… and its parent editor ## Description The image caption editor has its own `HistoryExtension`. `SharedHistoryExtension` was left commented out in meta#8202 with a FIXME saying the playground had tests that assumed captions did not share history. Before meta#8202 the caption shared the parent's history through `HistoryPlugin externalHistoryState`. With separate stacks, undo pressed inside the caption only reverts caption edits, so a parent edit made just before focusing the caption cannot be undone or redone from there (meta#4401). This switches the caption to `SharedHistoryExtension`, the same way `StickyNode` does, and removes the FIXME. Shared undo/redo now applies to the editor that changed since meta#9053, and no existing unit or e2e test depends on the caption having its own history. Sharing the stack makes one new state reachable: a second undo inside the caption reverts the parent's "show caption" entry, which unmounts the focused caption and drops focus to the document body, where the redo shortcut no longer reaches the editor. `ImageComponent` now returns focus to the parent editor when the caption container is removed while it holds focus. It focuses inside an update, as `$onEscape` does, so Lexical redraws the restored node selection; without that, WebKit typed into the start of the first paragraph. Chromium can still show a caret from the key event that ran the undo, and typing there behaves as it does after a plain click on the image. The caption also hides itself on blur when it is empty. With a shared stack that hide pushed an entry and cleared the redo stack, so clicking the toolbar's Redo button right after an undo in the caption lost the redo. Outside collab the hide is now applied with `HISTORIC_TAG`, as the toolbar does for updates that skip the history stack, so it is not recorded. Collab is left alone: there the Yjs undo manager owns history, and `HISTORIC_TAG` would also keep the hide from syncing to the other clients. One step is still silent: after the toolbar Redo puts text back into the hidden caption, the next undo and redo change only that hidden caption. Closes meta#4401 ## Changeset None needed: the change is limited to the playground. ## Test plan New tests in `Images.spec.mjs` (rich text, non-collab): undo/redo from the caption reaches a parent edit and leaves the caption text alone; the meta#4401 sequence (parent edit, caption edit, undo twice, redo twice); focus returns to the parent editor when an undo hides the focused caption, and typing then does not land in the paragraph (not checked on chromium, which inserts text with a node selection even after a plain click on the image); hiding an empty caption on blur keeps the redo stack (this one passes on `main` too and guards against the shared stack clearing redo). In collab, a new test checks that hiding an empty caption on blur reaches the other client. ### Before `pnpm exec cross-env E2E_BROWSER=chromium playwright test --project=chromium --retries=0 packages/lexical-playground/__tests__/e2e/Images.spec.mjs -g "Image caption history"` with `ImageNode.tsx` and `ImageComponent.tsx` from `main`: ``` 1) ... › Focus returns to the parent editor when an undo hides the focused caption Error: expect(locator).toHaveCount(expected) failed Expected: 0 Received: 1 2) ... › Undo and redo from the caption reach a parent edit Error: expect(locator).toHaveText(expected) failed Expected: "Hello" Received: "Hello world" 3) ... › Undo and redo walk the parent and caption edits in order Error: expect(locator).toHaveText(expected) failed Expected: "Hello" Received: "Hello world" 3 failed 1 passed (8.1s) ``` ### After ``` 4 passed (3.4s) ``` The new tests pass on chromium, webkit and firefox (3 runs each) and against the production build (`E2E_PORT=4000`, 2 runs each). Removing only the focus handling fails the focus test on all three browsers; focusing the root without redrawing the selection fails it on webkit; recording the blur hide in history fails the redo-stack test on all three browsers; tagging the hide in collab too fails the collab test; reverting only `ImageNode.tsx` fails the three undo/redo and focus tests on all three browsers. - `pnpm run tsc`, `pnpm run tsc-test`, `pnpm run prettier`: pass - `pnpm run test-unit`: 585 files, 18158 passed - `pnpm run test-e2e-chromium`: 640 passed, 3 failed. The 3 failures (`List.spec.mjs:520`, `Tables.spec.mjs:1422`, `TextFormatting.spec.mjs:859`) fail the same way on `main` in chromium, firefox, webkit and collab. - firefox and webkit, and collab chromium, firefox and webkit, on the image, history, links, selection, select-block, toolbar and image copy/paste specs: pass - Not exercised: collab v2 skips `Images.spec.mjs` (nested editors are not supported there), and captions are read-only in collab Y; the full collab Y suite passes.
mayrang
requested review from
acywatson,
etrepum,
ivailop7,
potatowagon and
zurfyx
as code owners
October 30, 2026 05:59
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
There was a problem hiding this comment.
This looks good, and thanks for picking up #4401. Switching the caption to SharedHistoryExtension, as StickyNode already does, puts undo and redo in the caption in one order with the parent. The focus hand-back covers the new case where an undo removes the focused caption, and the new tests in Images.spec.mjs fail against main and pass with this change. No changeset is needed: CONTRIBUTING.md and AGENTS.md both exempt playground changes.
One optional, non-blocking suggestion:
- Collab caption undo changes as well (
packages/lexical-playground/src/nodes/ImageNode.tsx:71). In collab v1/v2 the parent editor'sHistoryExtensionis configured withdisabled: isCollab(App.tsx), andSharedHistoryExtensioncopies that setting. As a result, undo inside a caption is now handled only by the caption's Yjs undo manager. That looks like an improvement. Onmain, undo in a collab caption restored the caption locally withHISTORIC_TAG, which@lexical/yjsdoes not sync, so the other client kept the old text. The description says collab is left alone, though, and no test covers this. Consider adding a case to the "Image caption in collab" block that types in the caption, undoes, and asserts the caption text in both frames, plus a sentence about it in the description.
etrepum
approved these changes
Nov 1, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The image caption editor has its own
HistoryExtension.SharedHistoryExtensionwas left commented out in #8202 with a FIXME saying the playground had tests that assumed captions did not share history. Before #8202 the caption shared the parent's history throughHistoryPlugin externalHistoryState. With separate stacks, undo pressed inside the caption only reverts caption edits, so a parent edit made just before focusing the caption cannot be undone or redone from there (#4401).This switches the caption to
SharedHistoryExtension, the same wayStickyNodedoes, and removes the FIXME. Shared undo/redo now applies to the editor that changed since #9053, and no existing unit or e2e test depends on the caption having its own history.Sharing the stack makes one new state reachable: a second undo inside the caption reverts the parent's "show caption" entry, which unmounts the focused caption and drops focus to the document body, where the redo shortcut no longer reaches the editor.
ImageComponentnow returns focus to the parent editor when the caption container is removed while it holds focus. It focuses inside an update, as$onEscapedoes, so Lexical redraws the restored node selection; without that, WebKit typed into the start of the first paragraph. Chromium can still show a caret from the key event that ran the undo, and typing there behaves as it does after a plain click on the image.The caption also hides itself on blur when it is empty. With a shared stack that hide pushed an entry and cleared the redo stack, so clicking the toolbar's Redo button right after an undo in the caption lost the redo. Outside collab the hide is now applied with
HISTORIC_TAG, as the toolbar does for updates that skip the history stack, so it is not recorded. Collab is left alone: there the Yjs undo manager owns history, andHISTORIC_TAGwould also keep the hide from syncing to the other clients. One step is still silent: after the toolbar Redo puts text back into the hidden caption, the next undo and redo change only that hidden caption.Closes #4401
Changeset
None needed: the change is limited to the playground.
Test plan
New tests in
Images.spec.mjs(rich text, non-collab): undo/redo from the caption reaches a parent edit and leaves the caption text alone; the #4401 sequence (parent edit, caption edit, undo twice, redo twice); focus returns to the parent editor when an undo hides the focused caption, and typing then does not land in the paragraph (not checked on chromium, which inserts text with a node selection even after a plain click on the image); hiding an empty caption on blur keeps the redo stack (this one passes onmaintoo and guards against the shared stack clearing redo). In collab, a new test checks that hiding an empty caption on blur reaches the other client.Before
pnpm exec cross-env E2E_BROWSER=chromium playwright test --project=chromium --retries=0 packages/lexical-playground/__tests__/e2e/Images.spec.mjs -g "Image caption history"withImageNode.tsxandImageComponent.tsxfrommain:After
The new tests pass on chromium, webkit and firefox (3 runs each) and against the production build (
E2E_PORT=4000, 2 runs each). Removing only the focus handling fails the focus test on all three browsers; focusing the root without redrawing the selection fails it on webkit; recording the blur hide in history fails the redo-stack test on all three browsers; tagging the hide in collab too fails the collab test; reverting onlyImageNode.tsxfails the three undo/redo and focus tests on all three browsers.pnpm run tsc,pnpm run tsc-test,pnpm run prettier: passpnpm run test-unit: 585 files, 18158 passedpnpm run test-e2e-chromium: 640 passed, 3 failed. The 3 failures (List.spec.mjs:520,Tables.spec.mjs:1422,TextFormatting.spec.mjs:859) fail the same way onmainin chromium, firefox, webkit and collab.Images.spec.mjs(nested editors are not supported there), and captions are read-only in collab Y; the full collab Y suite passes.