Skip to content

[bug]: Left-clicking a link in a read-only comment/description opens two tabs #9386

Description

@Maziak2520

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

Left-clicking any link (e.g. a PR URL) inside a displayed/read-only comment or work-item description opens the link in two identical browser tabs instead of one. It happens on every left-click, in every browser, so it is not a browser or extension issue.

It only affects links in read-only rendered content. While the same editor is in edit mode, links open a single tab correctly.

Expected: one click → one tab.

Root cause

The custom link extension has two behaviours that both fire for a read-only editor:

  1. openOnClick defaults to true, so the click-handler ProseMirror plugin is always registered — there is no read-only override anywhere in the codebase:
    packages/editor/src/core/extensions/custom-link/extension.tsxaddOptions() returns openOnClick: true, and addProseMirrorPlugins() pushes clickHandler(...) whenever openOnClick is truthy.

  2. The click handler calls window.open(...) but never calls event.preventDefault(), so the browser also follows the native <a href target="_blank">:

handleClick: (view, pos, event) => {
if (event.button !== 0) {
return false;
}
let a = event.target as HTMLElement;
const els: HTMLElement[] = [];
while (a?.nodeName !== "DIV") {
els.push(a);
a = a?.parentNode as HTMLElement;
}
if (!els.find((value) => value.nodeName === "A")) {
return false;
}
const attrs = getAttributes(view.state, options.type.name);
const link = event.target as HTMLLinkElement;
const href = link?.href ?? attrs.href;
const target = link?.target ?? attrs.target;
if (link && href) {
// Defence-in-depth: link.href is the browser-resolved URL (whitespace
// already stripped by the browser's WHATWG URL parser), so a protocol
// check here is sufficient to catch any dangerous URI that slipped past
// the editor's parse/render-time guards. Matches the blocked-scheme list
// in isValidHttpUrl (javascript:, data:, vbscript:, file:, about:)
// to keep the policy consistent (GHSA-v2vv-7wq3-8w2j).
if (/^(javascript|data|vbscript|file|about):/i.test(href)) {
return false;
}
window.open(href, target);
return true;
}

handleClick: (view, pos, event) => {
  if (event.button !== 0) {
    return false;
  }
  // ...walk up to the <a>...
  const href = link?.href ?? attrs.href;
  const target = link?.target ?? attrs.target;

  if (link && href) {
    window.open(href, target);   // tab #1
    return true;                 // no event.preventDefault() -> native anchor opens tab #2
  }
  return false;
},

In an editable view ProseMirror suppresses the native click (contenteditable), so only window.open fires → one tab. In a non-editable view the native anchor navigation is not suppressed, so the window.open tab and the native target="_blank" tab both open → two tabs. This is the same class of bug as TipTap's @tiptap/extension-link issue #4877 ("links open twice when the editor is not editable and openOnClick is true").

Suggested fix

Call event.preventDefault() before window.open(...) in clickHandler.ts, so the native anchor navigation is suppressed in both editable and read-only modes:

if (link && href) {
  event.preventDefault();
  window.open(href, target);
  return true;
}

(A non-left-click already returns early via if (event.button !== 0) return false, which is why middle-click / "open in new tab" correctly opens a single tab today — a useful workaround in the meantime.)

Steps to reproduce

  1. Open any work item that has a comment (or description) containing a hyperlink, e.g. a PR URL.
  2. View the item so the comment/description is rendered read-only (not in edit mode).
  3. Left-click the link.
  4. Two identical tabs open instead of one.

Environment

Self-hosted

Browser

Google Chrome

Variant

Self-hosted

Version

v1.3.1 (self-hosted community). Also confirmed present on the preview branch as of 2026-07-09 (commit 73e3608).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions