Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {selectAll} from '../keyboardShortcuts/index.mjs';
import {
assertHTML,
assertSelection,
focusEditor,
html,
initialize,
test,
} from '../utils/index.mjs';

/**
* Safari fires compositionend before keydown (unlike Chrome/Firefox).
* Dispatching this event sets isSafariEndingComposition = true in LexicalEvents.ts,
* which is the stale flag that causes the bug.
*/
async function dispatchCompositionEnd(page) {
await page.evaluate(() => {
document.querySelector('[contenteditable="true"]').dispatchEvent(
new CompositionEvent('compositionend', {
bubbles: true,
cancelable: false,
data: 'あああ',
}),
);
});
}

test.describe('Regression #8153', () => {
test.beforeEach(({isCollab, page}) => initialize({isCollab, page}));

test('Can delete all text selected with Cmd+A after IME composition end on Safari', async ({
page,
browserName,
isPlainText,
isCollab,
}) => {
test.skip(browserName !== 'webkit');
test.skip(isPlainText);
test.skip(isCollab);

await focusEditor(page);
await page.keyboard.type('Hello');
await page.keyboard.press('Enter');
await page.keyboard.type('World');

await dispatchCompositionEnd(page);

await selectAll(page);
await page.keyboard.press('Backspace');

await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph" dir="auto"><br /></p>
`,
);
await assertSelection(page, {
anchorOffset: 0,
anchorPath: [0],
focusOffset: 0,
focusPath: [0],
});
});

test('Can delete multi-paragraph selection with Shift+ArrowUp after IME composition end on Safari', async ({
page,
browserName,
isPlainText,
isCollab,
}) => {
test.skip(browserName !== 'webkit');
test.skip(isPlainText);
test.skip(isCollab);

await focusEditor(page);
await page.keyboard.type('Hello');
await page.keyboard.press('Enter');
await page.keyboard.type('World');
await page.keyboard.press('Enter');
await page.keyboard.type('あああ');

await dispatchCompositionEnd(page);

await page.keyboard.press('Shift+ArrowUp');
await page.keyboard.press('Shift+ArrowUp');
await page.keyboard.press('Backspace');

await assertHTML(
page,
html`
<p class="PlaygroundEditorTheme__paragraph" dir="auto">
<span data-lexical-text="true">Hello</span>
</p>
`,
);
});
});
14 changes: 9 additions & 5 deletions packages/lexical/src/LexicalEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,13 +1193,17 @@ function $handleKeyDown(event: KeyboardEvent): boolean {
if (event.key == null) {
return true;
}
if (isSafariEndingComposition && isBackspace(event)) {
updateEditorSync(editor, () => {
$onCompositionEndImpl(editor, safariEndCompositionEventData);
});
if (isSafariEndingComposition) {
if (isBackspace(event)) {
updateEditorSync(editor, () => {
$onCompositionEndImpl(editor, safariEndCompositionEventData);
});
isSafariEndingComposition = false;
safariEndCompositionEventData = '';
return true;
}
isSafariEndingComposition = false;
safariEndCompositionEventData = '';
return true;
}

if (isMoveForward(event)) {
Expand Down
34 changes: 26 additions & 8 deletions packages/lexical/src/LexicalSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2979,6 +2979,19 @@ function setDOMSelectionBaseAndExtent(
}
}

function getElementAndOffsetForPoint(
editor: LexicalEditor,
node: LexicalNode,
offset: number,
): [HTMLElement, number] {
const element = getElementByKeyOrThrow(editor, node.getKey());
if ($isElementNode(node)) {
const slot = node.getDOMSlot(element);
return [slot.element, offset + slot.getFirstChildOffset()];
}
return [element, offset];
}

export function updateDOMSelection(
prevSelection: BaseSelection | null,
nextSelection: BaseSelection | null,
Expand Down Expand Up @@ -3021,12 +3034,18 @@ export function updateDOMSelection(

const anchor = nextSelection.anchor;
const focus = nextSelection.focus;
const anchorKey = anchor.key;
const focusKey = focus.key;
const anchorDOM = getElementByKeyOrThrow(editor, anchorKey);
const focusDOM = getElementByKeyOrThrow(editor, focusKey);
const nextAnchorOffset = anchor.offset;
const nextFocusOffset = focus.offset;
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const [anchorDOM, nextAnchorOffset] = getElementAndOffsetForPoint(
editor,
anchorNode,
anchor.offset,
);
const [focusDOM, nextFocusOffset] = getElementAndOffsetForPoint(
editor,
focusNode,
focus.offset,
);
const nextFormat = nextSelection.format;
const nextStyle = nextSelection.style;
const isCollapsed = nextSelection.isCollapsed();
Expand All @@ -3036,7 +3055,6 @@ export function updateDOMSelection(

if (anchor.type === 'text') {
nextAnchorNode = getDOMTextNode(anchorDOM);
const anchorNode = anchor.getNode();
anchorFormatOrStyleChanged =
anchorNode.getFormat() !== nextFormat ||
anchorNode.getStyle() !== nextStyle;
Expand Down Expand Up @@ -3069,7 +3087,7 @@ export function updateDOMSelection(
nextFormat,
nextStyle,
nextAnchorOffset,
anchorKey,
anchor.key,
performance.now(),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,28 @@ describe('getDOMSlot tests', () => {
`<main dir="auto"><section><br></section></main>`,
);
});

test('DOM selection uses getDOMSlot element for element selections', () => {
editor.update(
() => {
const wrapper = $createWrapperElementNode().append(
$createParagraphNode().append($createTextNode('A')),
$createParagraphNode().append($createTextNode('B')),
$createParagraphNode().append($createTextNode('C')),
);
$getRoot().clear().append(wrapper);
// Create element-type selection on wrapper
wrapper.select(0, wrapper.getChildrenSize());
},
{discrete: true},
);

const domSelection = window.getSelection();
expect(domSelection!.anchorNode!.nodeName).toBe('SECTION');
expect(domSelection!.anchorOffset).toBe(0);
expect(domSelection!.focusNode!.nodeName).toBe('SECTION');
expect(domSelection!.focusOffset).toBe(3);
});
});

describe('indexPath', () => {
Expand Down