Skip to content
Open
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
30 changes: 28 additions & 2 deletions src/editor/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { $getListDepth, ListItemNode, ListNode } from "@lexical/list"
import { $getTableCellNodeFromLexicalNode, TableCellNode } from "@lexical/table"
import { CodeNode } from "@lexical/code"
import { isSelectionHighlighted } from "../helpers/format_helper"
import { caretFromPoint } from "../helpers/caret_helpers"
import { getNonce } from "../helpers/csp_helper"
import { $createNodeSelectionWith, $isListItemStructurallyEmpty, getListType } from "../helpers/lexical_helper"
import { LinkNode } from "@lexical/link"
Expand Down Expand Up @@ -375,11 +376,16 @@ export default class Selection {
}

#listenForNodeSelections() {
this.#listeners.track(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
this.#listeners.track(this.editor.registerCommand(CLICK_COMMAND, (event) => {
const { target } = event
if (!isDOMNode(target)) return false

const targetNode = $getNearestNodeFromDOMNode(target)
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
if ($isDecoratorNode(targetNode)) {
return this.#selectInLexical(targetNode)
} else {
return this.#placeCaretBeforeInlineDecorator(event)
}
}, COMMAND_PRIORITY_LOW))

this.#listeners.track(
Expand All @@ -391,6 +397,26 @@ export default class Selection {
)
}

// Gecko resolves a click just before an inline decorator to a text node inside
// its contenteditable="false" subtree, which maps to no selection at all, so
// typing is silently dropped. Place the caret before the decorator ourselves.
#placeCaretBeforeInlineDecorator(event) {
if ($getSelection() !== null) return false

const position = caretFromPoint(event.clientX, event.clientY)
if (!position) return false

const node = $getNearestNodeFromDOMNode(position.node)
if (!$isDecoratorNode(node) || !node.isInline()) return false

const parent = node.getParent()
if (!$isElementNode(parent)) return false

const index = node.getIndexWithinParent()
parent.select(index, index)
return true
}

#containEditorFocus() {
// Workaround for a bizarre Chrome bug where the cursor abandons the editor to focus on not-focusable elements
// above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
Expand Down
55 changes: 55 additions & 0 deletions test/browser/tests/prompts/typing_before_mention.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test } from "../../test_helper.js"
import { expect } from "@playwright/test"

async function insertMention(page, editor) {
await editor.send("@")
await expect(page.locator(".lexxy-prompt-menu--visible")).toBeVisible({ timeout: 5_000 })
await editor.send("Enter")
await editor.flush()
await expect(editor.content.locator("action-text-attachment")).toBeVisible({ timeout: 5_000 })
}

async function clickAt(page, editor, clientX, clientY) {
await page.mouse.click(clientX, clientY)
await editor.flush()
}

test.describe("Typing beside a mention", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/mentions.html")
await page.waitForSelector("lexxy-editor[connected]")
})

test("clicking before a leading mention places the caret before it", async ({ page, editor }) => {
await insertMention(page, editor)

const mention = await editor.content.locator("action-text-attachment").boundingBox()
const content = await editor.content.boundingBox()
await clickAt(page, editor, content.x + 1, mention.y + mention.height / 2)

await editor.send("Hi")
await editor.flush()

await expect(editor.content.locator("action-text-attachment")).toHaveCount(1)
const value = await editor.value()
expect(value).toContain("Hi")
expect(value.indexOf("Hi")).toBeLessThan(value.indexOf("<action-text-attachment"))
})

test("clicking after a trailing mention places the caret after it", async ({ page, editor }) => {
await insertMention(page, editor)

// Well past the trailing space Lexxy inserts after a mention, so the click
// lands clearly after the mention rather than on that space.
const mention = await editor.content.locator("action-text-attachment").boundingBox()
await clickAt(page, editor, mention.x + mention.width + 40, mention.y + mention.height / 2)

await editor.send("Hi")
await editor.flush()

await expect(editor.content.locator("action-text-attachment")).toHaveCount(1)
const value = await editor.value()
expect(value).toContain("Hi")
expect(value.indexOf("Hi")).toBeGreaterThan(value.indexOf("<action-text-attachment"))
})
})