Skip to content

[lexical-link] Bug Fix: AutoLinkNode.insertNewAfter keeps the node's own properties - #8996

Merged
etrepum merged 1 commit into
facebook:mainfrom
LeSingh1:fix/autolink-insertnewafter-copy
Aug 9, 2026
Merged

[lexical-link] Bug Fix: AutoLinkNode.insertNewAfter keeps the node's own properties#8996
etrepum merged 1 commit into
facebook:mainfrom
LeSingh1:fix/autolink-insertnewafter-copy

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Description

LinkNode.insertNewAfter produces the trailing half of a split with
$copyNode, which runs constructor.clone(node) followed by
afterCloneFrom(node):

insertNewAfter(_: RangeSelection, restoreSelection = true): null | ElementNode {
  const linkNode = $copyNode(this);
  this.insertAfter(linkNode, restoreSelection);
  return linkNode;
}

AutoLinkNode overrode it with a hand-rolled constructor call that enumerated
four properties:

const linkNode = $createAutoLinkNode(this.__url, {
  isUnlinked: this.__isUnlinked,
  rel: this.__rel,
  target: this.__target,
  title: this.__title,
});

Everything else the clone contract carries was dropped:

  • __format, __indent, __style, __dir, __textFormat, __textStyle,
    carried by ElementNode.afterCloneFrom;
  • __state — the entire NodeState, i.e. every createState/$setState value,
    carried by LexicalNode.afterCloneFrom;
  • the concrete class: $copyNode goes through node.constructor.clone, while
    $createAutoLinkNode hardcodes new AutoLinkNode(...), so a subclass of
    AutoLinkNode came back as a base AutoLinkNode.

The production path is $splitNodeAtPoint
(packages/lexical/src/LexicalSelection.ts, node.insertNewAfter(insertPoint)),
so splitting an autolink while inserting or pasting produced a trailing half
that had silently lost its node state and element props — while the identical
operation on a plain LinkNode preserved them.

The override is also redundant: AutoLinkNode.afterCloneFrom already carries
__isUnlinked, and LinkNode.afterCloneFrom carries url/rel/target/title. So
the fix is to delete it and inherit the correct implementation.

Test plan

Three new cases in
packages/lexical-link/src/__tests__/unit/LexicalAutoLinkNode.test.ts. The
third is a control that passes before and after — it pins the four attributes
the override did handle, so removing it cannot regress them.

The existing AutoLinkNode.insertNewAfter does not create new paragraph test
asserts only node type and sibling counts, all of which still hold.

Before

$ npx vitest run packages/lexical-link/src/__tests__/unit/LexicalAutoLinkNode.test.ts

     × keeps the element style, text style and text format 6ms
     × keeps the NodeState 2ms
     ✓ keeps the link attributes (control) 1ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected '' to be 'color: red' // Object.is equality
AssertionError: expected '' to be 'carried' // Object.is equality

      Tests  2 failed | 29 passed (31)

After

$ npx vitest run packages/lexical-link packages/lexical-playground packages/lexical/src

 Test Files  88 passed (88)
      Tests  1773 passed | 1 skipped (1774)

npx tsc -p tsconfig.json --noEmit and npx eslint on the changed file are
both clean.

Note: this touches LexicalLinkNode.ts, as does #8993, but a different method
(AutoLinkNode.insertNewAfter vs the $toggleLink NodeSelection branch). The
two apply cleanly in either order.

…own properties

## Description

`LinkNode.insertNewAfter` produces the trailing half of a split with
`$copyNode`, which runs `constructor.clone(node)` followed by
`afterCloneFrom(node)`:

```ts
insertNewAfter(_: RangeSelection, restoreSelection = true): null | ElementNode {
  const linkNode = $copyNode(this);
  this.insertAfter(linkNode, restoreSelection);
  return linkNode;
}
```

`AutoLinkNode` overrode it with a hand-rolled constructor call that enumerated
four properties:

```ts
const linkNode = $createAutoLinkNode(this.__url, {
  isUnlinked: this.__isUnlinked,
  rel: this.__rel,
  target: this.__target,
  title: this.__title,
});
```

Everything else the clone contract carries was dropped:

- `__format`, `__indent`, `__style`, `__dir`, `__textFormat`, `__textStyle`,
  carried by `ElementNode.afterCloneFrom`;
- `__state` — the entire NodeState, i.e. every `createState`/`$setState` value,
  carried by `LexicalNode.afterCloneFrom`;
- the concrete class: `$copyNode` goes through `node.constructor.clone`, while
  `$createAutoLinkNode` hardcodes `new AutoLinkNode(...)`, so a subclass of
  `AutoLinkNode` came back as a base `AutoLinkNode`.

The production path is `$splitNodeAtPoint`
(`packages/lexical/src/LexicalSelection.ts`, `node.insertNewAfter(insertPoint)`),
so splitting an autolink while inserting or pasting produced a trailing half
that had silently lost its node state and element props — while the identical
operation on a plain `LinkNode` preserved them.

The override is also redundant: `AutoLinkNode.afterCloneFrom` already carries
`__isUnlinked`, and `LinkNode.afterCloneFrom` carries url/rel/target/title. So
the fix is to delete it and inherit the correct implementation.

## Test plan

Three new cases in
`packages/lexical-link/src/__tests__/unit/LexicalAutoLinkNode.test.ts`. The
third is a control that passes before and after — it pins the four attributes
the override did handle, so removing it cannot regress them.

The existing `AutoLinkNode.insertNewAfter does not create new paragraph` test
asserts only node type and sibling counts, all of which still hold.

### Before

```
$ npx vitest run packages/lexical-link/src/__tests__/unit/LexicalAutoLinkNode.test.ts

     × keeps the element style, text style and text format 6ms
     × keeps the NodeState 2ms
     ✓ keeps the link attributes (control) 1ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected '' to be 'color: red' // Object.is equality
AssertionError: expected '' to be 'carried' // Object.is equality

      Tests  2 failed | 29 passed (31)
```

### After

```
$ npx vitest run packages/lexical-link packages/lexical-playground packages/lexical/src

 Test Files  88 passed (88)
      Tests  1773 passed | 1 skipped (1774)
```

`npx tsc -p tsconfig.json --noEmit` and `npx eslint` on the changed file are
both clean.

Note: this touches `LexicalLinkNode.ts`, as does facebook#8993, but a different method
(`AutoLinkNode.insertNewAfter` vs the `$toggleLink` NodeSelection branch). The
two apply cleanly in either order.
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

@LeSingh1 is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@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 Aug 9, 2026
@etrepum
etrepum added this pull request to the merge queue Aug 9, 2026
Merged via the queue into facebook:main with commit 19e00de Aug 9, 2026
44 of 46 checks passed
@etrepum etrepum mentioned this pull request Aug 31, 2026
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants