From 1b17df9e0ba43978c52e3f05fdab44c97bd29896 Mon Sep 17 00:00:00 2001 From: Tobias Odendahl Date: Tue, 29 Apr 2025 18:54:06 +0200 Subject: [PATCH] fix(richtext-lexical): ensure state is up-to-date on inline-block restore (#12128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Ensures that the initial state on inline blocks gets updated when an inline block gets restored from lexical history. ### Why? If an inline block got edited, removed, and restored (via lexical undo), the state of the inline block was taken from an outdated initial state and did not reflect the current form state, see screencast https://github.com/user-attachments/assets/6f55ded3-57bc-4de0-8ac1-e49331674d5f ### How? We now ensure that the initial state gets re-initialized after the component got unmounted, resulting in the expected behavior: https://github.com/user-attachments/assets/4e97eeb2-6dc4-49b1-91ca-35b59a93a348 --------- Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com> --- .../blocks/client/componentInline/index.tsx | 14 +++++- .../Lexical/e2e/blocks/e2e.spec.ts | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx b/packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx index 9df01e47f9..8cae0bdd1b 100644 --- a/packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx @@ -284,10 +284,22 @@ export const InlineBlockComponent: React.FC = (props) => { ) // cleanup effect useEffect(() => { + const isStateOutOfSync = (formData: InlineBlockFields, initialState: FormState) => { + return Object.keys(initialState).some( + (key) => initialState[key] && formData[key] !== initialState[key].value, + ) + } + return () => { + // If the component is unmounted (either via removeInlineBlock or via lexical itself) and the form state got changed before, + // we need to reset the initial state to force a re-fetch of the initial state when it gets mounted again (e.g. via lexical history undo). + // Otherwise it would use an outdated initial state. + if (initialState && isStateOutOfSync(formData, initialState)) { + setInitialState(false) + } abortAndIgnore(onChangeAbortControllerRef.current) } - }, []) + }, [formData, initialState]) /** * HANDLE FORM SUBMIT diff --git a/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts b/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts index b1c5ddc986..eac8657da0 100644 --- a/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts +++ b/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts @@ -1666,5 +1666,55 @@ describe('lexicalBlocks', () => { }, }) }) + + test('ensure inline blocks restore their state after undoing a removal', async () => { + await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10') + + await page.locator('.cell-id a').first().click() + await page.waitForURL(`**/collections/LexicalInBlock/**`) + + // Wait for the page to be fully loaded and elements to be stable + await page.waitForLoadState('domcontentloaded') + + // Wait for the specific row to be visible and have its content loaded + const row2 = page.locator('#blocks-row-2') + await expect(row2).toBeVisible() + + // Get initial count and ensure it's stable + const inlineBlocks = page.locator('#blocks-row-2 .inline-block-container') + const inlineBlockCount = await inlineBlocks.count() + await expect(() => { + expect(inlineBlockCount).toBeGreaterThan(0) + }).toPass() + + const inlineBlockElement = inlineBlocks.first() + await inlineBlockElement.locator('.inline-block__editButton').first().click() + + await page.locator('.drawer--is-open #field-text').fill('value1') + await page.locator('.drawer--is-open button[type="submit"]').first().click() + + // remove inline block + await inlineBlockElement.click() + await page.keyboard.press('Backspace') + + // Check both that this specific element is removed and the total count decreased + await expect(inlineBlocks).toHaveCount(inlineBlockCount - 1) + + await page.keyboard.press('Escape') + + await inlineBlockElement.click() + + // Undo the removal using keyboard shortcut + await page.keyboard.press('ControlOrMeta+Z') + + // Wait for the block to be restored + await expect(inlineBlocks).toHaveCount(inlineBlockCount) + + // Open the drawer again + await inlineBlockElement.locator('.inline-block__editButton').first().click() + + // Check if the text field still contains 'value1' + await expect(page.locator('.drawer--is-open #field-text')).toHaveValue('value1') + }) }) })