Skip to content

Commit

Permalink
Fix discrete nested updates (facebook#6419)
Browse files Browse the repository at this point in the history
  • Loading branch information
zurfyx authored Jul 18, 2024
1 parent 85abcde commit 3fcf02d
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 3 deletions.
8 changes: 8 additions & 0 deletions packages/lexical/src/LexicalUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,14 @@ function processNestedUpdates(
if (options.skipTransforms) {
skipTransforms = true;
}
if (options.discrete) {
const pendingEditorState = editor._pendingEditorState;
invariant(
pendingEditorState !== null,
'Unexpected empty pending editor state on discrete nested update',
);
pendingEditorState._flushSync = true;
}

if (onUpdate) {
editor._deferred.push(onUpdate);
Expand Down
113 changes: 112 additions & 1 deletion packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
$createLineBreakNode,
$createNodeSelection,
$createParagraphNode,
$createRangeSelection,
$createTextNode,
$getEditor,
$getNearestNodeFromDOMNode,
Expand Down Expand Up @@ -62,6 +63,7 @@ import {
$createTestElementNode,
$createTestInlineElementNode,
createTestEditor,
createTestHeadlessEditor,
TestComposer,
TestTextNode,
} from '../utils';
Expand Down Expand Up @@ -307,9 +309,11 @@ describe('LexicalEditor tests', () => {

it('Should handle nested updates in the correct sequence', async () => {
init();
const onUpdate = jest.fn();

let log: Array<string> = [];

editor.registerUpdateListener(onUpdate);
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
Expand Down Expand Up @@ -354,6 +358,7 @@ describe('LexicalEditor tests', () => {
// Wait for update to complete
await Promise.resolve().then();

expect(onUpdate).toHaveBeenCalledTimes(1);
expect(log).toEqual(['A1', 'B1', 'C1', 'D1', 'E1', 'F1']);

log = [];
Expand Down Expand Up @@ -447,6 +452,28 @@ describe('LexicalEditor tests', () => {
]);
});

it('nested update after selection update triggers exactly 1 update', async () => {
init();
const onUpdate = jest.fn();
editor.registerUpdateListener(onUpdate);
editor.update(() => {
$setSelection($createRangeSelection());
editor.update(() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Sync update')),
);
});
});

await Promise.resolve().then();

const textContent = editor
.getEditorState()
.read(() => $getRoot().getTextContent());
expect(textContent).toBe('Sync update');
expect(onUpdate).toHaveBeenCalledTimes(1);
});

it('update does not call onUpdate callback when no dirty nodes', () => {
init();

Expand Down Expand Up @@ -2351,7 +2378,7 @@ describe('LexicalEditor tests', () => {
});
});

it('can use flushSync for synchronous updates', () => {
it('can use discrete for synchronous updates', () => {
init();
const onUpdate = jest.fn();
editor.registerUpdateListener(onUpdate);
Expand All @@ -2373,6 +2400,90 @@ describe('LexicalEditor tests', () => {
expect(onUpdate).toHaveBeenCalledTimes(1);
});

it('can use discrete after a non-discrete update to flush the entire queue', () => {
const headless = createTestHeadlessEditor();
const onUpdate = jest.fn();
headless.registerUpdateListener(onUpdate);
headless.update(() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Async update')),
);
});
headless.update(
() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Sync update')),
);
},
{
discrete: true,
},
);

const textContent = headless
.getEditorState()
.read(() => $getRoot().getTextContent());
expect(textContent).toBe('Async update\n\nSync update');
expect(onUpdate).toHaveBeenCalledTimes(1);
});

it('can use discrete after a non-discrete setEditorState to flush the entire queue', () => {
init();
editor.update(
() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Async update')),
);
},
{
discrete: true,
},
);

const headless = createTestHeadlessEditor(editor.getEditorState());
headless.update(
() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Sync update')),
);
},
{
discrete: true,
},
);
const textContent = headless
.getEditorState()
.read(() => $getRoot().getTextContent());
expect(textContent).toBe('Async update\n\nSync update');
});

it('can use discrete in a nested update to flush the entire queue', () => {
init();
const onUpdate = jest.fn();
editor.registerUpdateListener(onUpdate);
editor.update(() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Async update')),
);
editor.update(
() => {
$getRoot().append(
$createParagraphNode().append($createTextNode('Sync update')),
);
},
{
discrete: true,
},
);
});

const textContent = editor
.getEditorState()
.read(() => $getRoot().getTextContent());
expect(textContent).toBe('Async update\n\nSync update');
expect(onUpdate).toHaveBeenCalledTimes(1);
});

it('does not include linebreak into inline elements', async () => {
init();

Expand Down
6 changes: 4 additions & 2 deletions packages/lexical/src/__tests__/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,11 @@ export function createTestEditor(
return editor;
}

export function createTestHeadlessEditor(): LexicalEditor {
export function createTestHeadlessEditor(
editorState?: EditorState,
): LexicalEditor {
return createHeadlessEditor({
namespace: '',
editorState,
onError: (error) => {
throw error;
},
Expand Down

0 comments on commit 3fcf02d

Please sign in to comment.