Skip to content

[lexical-playground] Bug Fix: Share history between the image caption and its parent editor - #9566

Merged
etrepum merged 2 commits into
meta:mainfrom
mayrang:fix/caption-shared-history
Nov 1, 2026
Merged

etrepum merged 2 commits into
meta:mainfrom
mayrang:fix/caption-shared-history

Conversation

@mayrang

@mayrang mayrang commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Description

The image caption editor has its own HistoryExtension. SharedHistoryExtension was 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 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 (#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 #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 #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 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.

… 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.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 31, 2026 6:05pm UTC
lexical-playground Ready Ready Preview Oct 31, 2026 6:05pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 30, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9566

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9566

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9566

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9566

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9566

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9566

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9566

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9566

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9566

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9566

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9566

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9566

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9566

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9566

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9566

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9566

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9566

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9566

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9566

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9566

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9566

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9566

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9566

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9566

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9566

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9566

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9566

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9566

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9566

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9566

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9566

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9566

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9566

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9566

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9566

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9566

commit: 030650a

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Oct 31, 2026

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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's HistoryExtension is configured with disabled: isCollab (App.tsx), and SharedHistoryExtension copies 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. On main, undo in a collab caption restored the caption locally with HISTORIC_TAG, which @lexical/yjs does 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
etrepum added this pull request to the merge queue Nov 1, 2026
Merged via the queue into meta:main with commit 164af6a Nov 1, 2026
57 checks passed

This branch was successfully deployed

2 active deployments
Preview – lexical — 030650a6 Deployed Oct 31, 2026 by vercel[bot]
Preview – lexical-playground — 030650a6 Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: incorrect "redo" behavior with nested editor

2 participants