diff --git a/app/assets/javascripts/discourse/app/widgets/post-cooked.js b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
index 5a03038bc5d..57d631a71f2 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-cooked.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
@@ -370,6 +370,19 @@ export default class PostCooked {
cookedDiv.innerHTML = this.attrs.cooked;
}
+ // On WebKit-based browsers, triple clicking on the last paragraph of a post won't stop at the end of the paragraph.
+ // It looks like the browser is selecting EOL characters, and that causes the selection to leak into the following
+ // nodes until it finds a non-empty node. This is a workaround to prevent that from happening.
+ // We insert a div after the last paragraph at the end of the cooked content, containing a
element.
+ // The line break works as a barrier, causing the selection to stop at the correct place.
+ // To prevent layout shifts this div is styled to be invisible with height 0 and overflow hidden and set aria-hidden
+ // to true to prevent screen readers from reading it.
+ const selectionBarrier = document.createElement("div");
+ selectionBarrier.classList.add("cooked-selection-barrier");
+ selectionBarrier.ariaHidden = "true";
+ selectionBarrier.appendChild(document.createElement("br"));
+ cookedDiv.appendChild(selectionBarrier);
+
return cookedDiv;
}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js b/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js
index 43743d16d7f..9dbf379cb88 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/fast-edit-test.js
@@ -88,7 +88,7 @@ acceptance("Fast Edit", function (needs) {
await visit("/t/internationalization-localization/280");
query("#post_2 .cooked").append(`That’s what she said!`);
- const textNode = query("#post_2 .cooked").childNodes[2];
+ const textNode = query("#post_2 .cooked").childNodes[3];
await selectText(textNode);
await click(".quote-button .quote-edit-label");
@@ -101,7 +101,7 @@ acceptance("Fast Edit", function (needs) {
await visit("/t/internationalization-localization/280");
query("#post_2 .cooked").append(`Je suis désolé, comment ça va?`);
- const textNode = query("#post_2 .cooked").childNodes[2];
+ const textNode = query("#post_2 .cooked").childNodes[3];
await selectText(textNode);
await click(".quote-button .quote-edit-label");
@@ -113,7 +113,7 @@ acceptance("Fast Edit", function (needs) {
await visit("/t/internationalization-localization/280");
query("#post_2 .cooked").append(`这是一个测试`);
- const textNode = query("#post_2 .cooked").childNodes[2];
+ const textNode = query("#post_2 .cooked").childNodes[3];
await selectText(textNode);
await click(".quote-button .quote-edit-label");
@@ -125,7 +125,7 @@ acceptance("Fast Edit", function (needs) {
await visit("/t/internationalization-localization/280");
query("#post_2 .cooked").append(`This is great 👍`);
- const textNode = query("#post_2 .cooked").childNodes[2];
+ const textNode = query("#post_2 .cooked").childNodes[3];
await selectText(textNode);
await click(".quote-button .quote-edit-label");
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 6f3107b07bc..e7f64679262 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -244,6 +244,15 @@
}
}
+.cooked-selection-barrier {
+ height: 0;
+ margin: 0;
+ padding: 0;
+ border: none;
+ overflow: hidden;
+ opacity: 0;
+}
+
// add staff color
.moderator {
.regular > .cooked {
diff --git a/spec/system/topic_page_spec.rb b/spec/system/topic_page_spec.rb
index dcba74a1741..38c64a954b8 100644
--- a/spec/system/topic_page_spec.rb
+++ b/spec/system/topic_page_spec.rb
@@ -9,6 +9,7 @@ describe "Topic page", type: :system do
x
Testing
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer tempor.
HTML it "allows TOC anchor navigation" do @@ -98,4 +99,26 @@ describe "Topic page", type: :system do expect(find("#post_#{topic.highest_post_number}")).to be_visible end end + + context "when triple clicking to select a paragraph" do + it "select the last paragraph" do + visit "/t/#{topic.slug}/#{topic.id}/1" + + # select the last paragraph by triple clicking + element = page.driver.browser.find_element(id: "test-last-cooked-paragraph") + page.driver.browser.action.move_to(element).click.click.click.perform + + # get the selected text in the browser + select_content = page.evaluate_script("window.getSelection().toString()") + + # the browser is returning control characters among the whiter space in the end of the text + # this regex will work as a .rstrip on steroids and remove them + select_content.gsub!(/[\s\p{Cf}]+$/, "") + + # compare the selected text with the last paragraph + expect(select_content).to eq( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer tempor.", + ) + end + end end