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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default defineConfig({
"**/composer-link-shortcut.spec.ts",
"**/entity-link-recipient-cards.spec.ts",
"**/composer-selection-formatting.spec.ts",
"**/composer-triple-click-selection.spec.ts",
"**/composer-tooltip-dismiss.spec.ts",
"**/mentions.spec.ts",
"**/team-mentions.spec.ts",
Expand Down
42 changes: 42 additions & 0 deletions desktop/src/features/messages/lib/useRichTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,48 @@ export function useRichTextEditor({
};
},
}),
// Triple-click selects only the clicked *line* (the text between
// the nearest hard breaks), not the whole paragraph. Shift+Enter
// never starts a new paragraph — a multi-line typed message is one
// <p> containing inline <br> (hardBreak) nodes — so the browser's
// native "expand triple-click to nearest block" resolves to the
// entire message. Override it to match textarea/Slack-style
// per-line selection. Pasted multi-line text is unaffected: it
// parses into separate <p> nodes via TiptapMarkdown below, so the
// browser default already does the right thing there.
Extension.create({
name: "tripleClickSelectsLine",
addProseMirrorPlugins() {
return [
new Plugin({
props: {
handleTripleClick(view, pos) {
const $pos = view.state.doc.resolve(pos);
if (!$pos.parent.inlineContent) return false;

const { start, end } = hardBreakLineBounds($pos);
const parentStart = $pos.start();
const parentEnd = parentStart + $pos.parent.content.size;
// No hard breaks in this block (e.g. a single-line
// paragraph, a pasted block's own <p>, or a code
// block) -> identical to the default paragraph-wide
// selection. Let ProseMirror handle it natively.
if (start === parentStart && end === parentEnd) {
return false;
}

view.dispatch(
view.state.tr.setSelection(
TextSelection.create(view.state.doc, start, end),
),
);
return true;
},
},
}),
];
},
}),
// Shift+Enter inside lists/blockquotes: split the node instead of
// inserting a hard break so continuation lines keep their formatting.
Extension.create({
Expand Down
56 changes: 56 additions & 0 deletions desktop/tests/e2e/composer-triple-click-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test, type Locator, type Page } from "@playwright/test";

import { installMockBridge } from "../helpers/bridge";

async function openGeneral(page: Page) {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
}

async function tripleClickText(page: Page, input: Locator, text: string) {
const point = await input.evaluate((element, targetText) => {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);

while (walker.nextNode()) {
const node = walker.currentNode;
const value = node.textContent ?? "";
const index = value.indexOf(targetText);
if (index < 0) continue;

const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + targetText.length);
const rect = range.getBoundingClientRect();
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}

throw new Error(`Could not locate "${targetText}" for triple-click`);
}, text);

await page.mouse.click(point.x, point.y, { clickCount: 3 });
}

test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});

test("triple-clicking a composer line selects only that line, not the whole message", async ({
page,
}) => {
await openGeneral(page);

const input = page.getByTestId("message-input");
await input.click();
await input.pressSequentially("before");
await input.press("Shift+Enter");
await input.pressSequentially("selected line");
await input.press("Shift+Enter");
await input.pressSequentially("after");

await tripleClickText(page, input, "selected line");

await expect
.poll(() => page.evaluate(() => window.getSelection()?.toString()))
.toBe("selected line");
});