From c561909c39da824a4c5eac011b9e5ffd27d92f58 Mon Sep 17 00:00:00 2001 From: lyubomir-bozhinov Date: Fri, 17 Jul 2026 17:32:40 +0300 Subject: [PATCH] Keep the selection when a drag crosses a custom attachment Dragging a selection across a mention wiped the whole selection in Firefox: the drag ended with no range at all, so nothing was selected and the content could not be copied or replaced. Gecko will not extend a selection across a contenteditable="false" island unless the island is selectable as a single unit. Marking custom attachments user-select: all makes the selection span them, and the mention's text is included in it. Scope the rule to the editor (lexxy-editor &) so it does not change text selection in rendered, read-only Action Text content, which has no contenteditable island and so never hits the Gecko bug. --- app/assets/stylesheets/lexxy-content.css | 11 +++++ .../prompts/selection_across_mention.test.js | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 test/browser/tests/prompts/selection_across_mention.test.js diff --git a/app/assets/stylesheets/lexxy-content.css b/app/assets/stylesheets/lexxy-content.css index 6a06d9539..bc8dc0fd1 100644 --- a/app/assets/stylesheets/lexxy-content.css +++ b/app/assets/stylesheets/lexxy-content.css @@ -478,6 +478,17 @@ position: relative; white-space: normal; + /* Gecko drops the whole selection when a drag crosses a contenteditable="false" + island unless it is selectable as a single unit. Editor-only: the rendered + view has no contenteditable and must keep normal text selection. */ + lexxy-editor & { + -webkit-user-select: all; + + @supports (user-select: all) { + user-select: all; + } + } + img { block-size: var(--lexxy-attachment-image-size); border-radius: 50%; diff --git a/test/browser/tests/prompts/selection_across_mention.test.js b/test/browser/tests/prompts/selection_across_mention.test.js new file mode 100644 index 000000000..c6aea6a7d --- /dev/null +++ b/test/browser/tests/prompts/selection_across_mention.test.js @@ -0,0 +1,47 @@ +import { test } from "../../test_helper.js" +import { expect } from "@playwright/test" + +const MENTION = '' + +async function dragAcrossParagraph(page, editor) { + const paragraph = await editor.content.locator("p").first().boundingBox() + const y = paragraph.y + paragraph.height / 2 + + await page.mouse.move(paragraph.x + 4, y) + await page.mouse.down() + await page.mouse.move(paragraph.x + paragraph.width - 4, y, { steps: 15 }) + await page.mouse.up() + await editor.flush() +} + +function selectedText(page) { + return page.evaluate(() => window.getSelection().toString()) +} + +test.describe("Selecting content that contains a mention", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/mentions.html") + await page.waitForSelector("lexxy-editor[connected]") + }) + + test("dragging across a mention selects the surrounding text", async ({ page, editor }) => { + await editor.setValue(`

Hello ${MENTION} world

`) + await editor.flush() + + await dragAcrossParagraph(page, editor) + + const selection = await selectedText(page) + expect(selection).toContain("Hello") + expect(selection).toContain("world") + expect(selection).toContain("Zacharias") + }) + + test("dragging across plain text still selects it", async ({ page, editor }) => { + await editor.setValue("

Hello there world

") + await editor.flush() + + await dragAcrossParagraph(page, editor) + + expect(await selectedText(page)).toContain("Hello there world") + }) +})