diff --git a/engine/app/assets/stylesheets/coplan/application.css b/engine/app/assets/stylesheets/coplan/application.css index ad37943..016acd3 100644 --- a/engine/app/assets/stylesheets/coplan/application.css +++ b/engine/app/assets/stylesheets/coplan/application.css @@ -1800,6 +1800,7 @@ img.avatar { background: var(--color-highlight-open-bg); border-bottom: 2px solid var(--color-highlight-open-border); cursor: pointer; + pointer-events: auto; } .anchor-highlight--open:hover { diff --git a/engine/app/javascript/controllers/coplan/text_selection_controller.js b/engine/app/javascript/controllers/coplan/text_selection_controller.js index 02f82e7..b153831 100644 --- a/engine/app/javascript/controllers/coplan/text_selection_controller.js +++ b/engine/app/javascript/controllers/coplan/text_selection_controller.js @@ -255,6 +255,7 @@ export default class extends Controller { } openThreadPopover(event) { + event.stopPropagation() this._showThreadPopoverFor(event.currentTarget, "pinned") } @@ -834,14 +835,6 @@ export default class extends Controller { // Skip zero-length ranges (e.g. from text node splits by prior highlights) if (localEnd <= localStart) continue - // If the text is already inside a highlight mark (from another thread - // anchored to the same text), reuse that mark instead of nesting. - const existingMark = tn.node.parentElement?.closest("mark.anchor-highlight") - if (existingMark) { - if (!marks.includes(existingMark)) marks.push(existingMark) - continue - } - const range = document.createRange() range.setStart(tn.node, localStart) range.setEnd(tn.node, localEnd) diff --git a/spec/system/comment_ux_spec.rb b/spec/system/comment_ux_spec.rb index fa40d87..1a00ab3 100644 --- a/spec/system/comment_ux_spec.rb +++ b/spec/system/comment_ux_spec.rb @@ -782,6 +782,66 @@ def create_anchored_thread(plan:, anchor_text:, body:, user:) describe "creating a new comment" do before { sign_in(author) } + it "keeps an overlapping heading comment visible and clickable after reload" do + plan.current_plan_version.update!(content_markdown: <<~MARKDOWN) + # Performance + + ## 1.3 Hard carts — and why native degrades fast + + Cart shapes come from what actually co-occurs in fleet traffic. + MARKDOWN + resolved = create_anchored_thread( + plan: plan, + anchor_text: "1.3 Hard carts — and why native degrades fast", + body: "Previously addressed feedback.", + user: author + ) + resolved.update!(status: "resolved", resolved_by_user: author) + visit plan_path(plan) + + page.execute_script <<~JS + const content = document.querySelector('[data-coplan--text-selection-target="content"]'); + const heading = content.querySelector('h2'); + const walker = document.createTreeWalker(heading, NodeFilter.SHOW_TEXT); + let node; + while ((node = walker.nextNode()) && !node.textContent.includes('Hard')) {} + if (!node) throw new Error('Hard text node not found'); + const start = node.textContent.indexOf('Hard'); + const range = document.createRange(); + range.setStart(node, start); + range.setEnd(node, start + 'Hard'.length); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + heading.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + JS + + expect(page).to have_css(".comment-popover", visible: true, wait: 3) + find(".comment-popover button").click + + within("#new-comment-form") do + textarea = find("textarea") + textarea.fill_in with: "This case needs more detail." + textarea.send_keys(:enter) + end + + expect(page).not_to have_css("#new-comment-form", visible: true, wait: 5) + thread = plan.comment_threads.reload.last + expect(thread.anchor_text).to eq("Hard") + expect(page).to have_css("mark.anchor-highlight", text: "Hard", wait: 5) + expect(page).to have_css("##{ActionView::RecordIdentifier.dom_id(thread)}") + expect(page).to have_css("mark[data-thread-id='#{ActionView::RecordIdentifier.dom_id(thread)}']") + + refresh + + expect(page).to have_css("mark.anchor-highlight", text: "Hard", wait: 5) + mark = find("mark[data-thread-id='#{ActionView::RecordIdentifier.dom_id(thread)}']") + expect(mark[:class]).to include("anchor-highlight--todo") + mark.click + expect(page).to have_css("##{ActionView::RecordIdentifier.dom_id(thread)}_popover", visible: true) + expect(page).to have_content("This case needs more detail.") + end + it "creates a thread via the text selection form" do visit plan_path(plan)