[lexical] Bug Fix: Point.isBefore could return incorrect result due to normalization (#7256)

This commit is contained in:
Bob Ippolito
2025-02-28 10:07:40 -08:00
committed by GitHub
parent f43807fc99
commit 99e64bc4fd
4 changed files with 66 additions and 22 deletions

View File

@ -98,10 +98,18 @@ export function $setBlocksType<T extends ElementNode>(
prevNode.replace(element, true); prevNode.replace(element, true);
if (newSelection) { if (newSelection) {
if (key === newSelection.anchor.key) { if (key === newSelection.anchor.key) {
newSelection.anchor.key = element.getKey(); newSelection.anchor.set(
element.getKey(),
newSelection.anchor.offset,
newSelection.anchor.type,
);
} }
if (key === newSelection.focus.key) { if (key === newSelection.focus.key) {
newSelection.focus.key = element.getKey(); newSelection.focus.set(
element.getKey(),
newSelection.focus.offset,
newSelection.focus.type,
);
} }
} }
} }

View File

@ -17,6 +17,7 @@ import invariant from 'shared/invariant';
import { import {
$caretFromPoint, $caretFromPoint,
$caretRangeFromSelection, $caretRangeFromSelection,
$comparePointCaretNext,
$createLineBreakNode, $createLineBreakNode,
$createParagraphNode, $createParagraphNode,
$createTextNode, $createTextNode,
@ -151,23 +152,12 @@ export class Point {
} }
isBefore(b: PointType): boolean { isBefore(b: PointType): boolean {
let aNode = this.getNode(); if (this.key === b.key) {
let bNode = b.getNode(); return this.offset < b.offset;
const aOffset = this.offset;
const bOffset = b.offset;
if ($isElementNode(aNode)) {
const aNodeDescendant = aNode.getDescendantByIndex<ElementNode>(aOffset);
aNode = aNodeDescendant != null ? aNodeDescendant : aNode;
} }
if ($isElementNode(bNode)) { const aCaret = $normalizeCaret($caretFromPoint(this, 'next'));
const bNodeDescendant = bNode.getDescendantByIndex<ElementNode>(bOffset); const bCaret = $normalizeCaret($caretFromPoint(b, 'next'));
bNode = bNodeDescendant != null ? bNodeDescendant : bNode; return $comparePointCaretNext(aCaret, bCaret) < 0;
}
if (aNode === bNode) {
return aOffset < bOffset;
}
return aNode.isBefore(bNode);
} }
getNode(): LexicalNode { getNode(): LexicalNode {

View File

@ -14,10 +14,13 @@ import {
ListNode, ListNode,
} from '@lexical/list'; } from '@lexical/list';
import { import {
$caretRangeFromSelection,
$comparePointCaretNext,
$createLineBreakNode, $createLineBreakNode,
$createParagraphNode, $createParagraphNode,
$createRangeSelection, $createRangeSelection,
$createTextNode, $createTextNode,
$getCaretInDirection,
$getRoot, $getRoot,
$getSelection, $getSelection,
$isParagraphNode, $isParagraphNode,
@ -1397,3 +1400,42 @@ describe('Regression #7173', () => {
}); });
}); });
}); });
describe('Regression #3181', () => {
initializeUnitTest((testEnv) => {
test('Point.isBefore edge case with mixed TextNode & ElementNode and matching descendants', () => {
testEnv.editor.update(
() => {
const paragraph = $createParagraphNode();
const targetText = $createTextNode('target').setMode('token');
$getRoot()
.clear()
.append(
paragraph.append(
$createTextNode('a').setMode('token'),
$createTextNode('b').setMode('token'),
targetText,
),
);
const selection = paragraph.select(2, 2);
selection.focus.set(targetText.getKey(), 1, 'text');
expect(selection).toMatchObject({
anchor: {key: paragraph.getKey(), offset: 2, type: 'element'},
focus: {key: targetText.getKey(), offset: 1, type: 'text'},
});
const caretRange = $caretRangeFromSelection(selection);
expect(
$comparePointCaretNext(
// These are no-op when isBefore is correct
$getCaretInDirection(caretRange.anchor, 'next'),
$getCaretInDirection(caretRange.focus, 'next'),
),
).toBe(-1);
expect(selection.anchor.isBefore(selection.focus)).toBe(true);
expect(selection.focus.isBefore(selection.anchor)).toBe(false);
},
{discrete: true},
);
});
});
});

View File

@ -39,6 +39,7 @@ import {
type TextNode, type TextNode,
} from '../nodes/LexicalTextNode'; } from '../nodes/LexicalTextNode';
import { import {
$comparePointCaretNext,
$getAdjacentChildCaret, $getAdjacentChildCaret,
$getCaretRange, $getCaretRange,
$getChildCaret, $getChildCaret,
@ -56,7 +57,7 @@ import {
* @returns a PointCaret for the point * @returns a PointCaret for the point
*/ */
export function $caretFromPoint<D extends CaretDirection>( export function $caretFromPoint<D extends CaretDirection>(
point: PointType, point: Pick<PointType, 'type' | 'key' | 'offset'>,
direction: D, direction: D,
): PointCaret<D> { ): PointCaret<D> {
const {type, key, offset} = point; const {type, key, offset} = point;
@ -154,10 +155,13 @@ export function $caretRangeFromSelection(
selection: RangeSelection, selection: RangeSelection,
): CaretRange { ): CaretRange {
const {anchor, focus} = selection; const {anchor, focus} = selection;
const direction = focus.isBefore(anchor) ? 'previous' : 'next'; const anchorCaret = $caretFromPoint(anchor, 'next');
const focusCaret = $caretFromPoint(focus, 'next');
const direction =
$comparePointCaretNext(anchorCaret, focusCaret) <= 0 ? 'next' : 'previous';
return $getCaretRange( return $getCaretRange(
$caretFromPoint(anchor, direction), $getCaretInDirection(anchorCaret, direction),
$caretFromPoint(focus, direction), $getCaretInDirection(focusCaret, direction),
); );
} }