Skip to content

Commit 5f568dc

Browse files
authored
fix(extension-link): prevent click handler from blocking non-link element selection (#7359)
* fix: early return false if its not a link * fix(extension-link): prevent click handler from blocking non-link element selection * use closest instead * add editor view dom as stop traversal * add editor view dom as stop traversal * add editor view dom as stop traversal
1 parent 9516ee0 commit 5f568dc

File tree

3 files changed

+84
-22
lines changed

3 files changed

+84
-22
lines changed

.changeset/fast-moons-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tiptap/extension-link': patch
3+
---
4+
5+
Fixed an issue where clicking on non-link elements (like images) required multiple clicks to select them. The link click handler now properly returns early when the clicked element is not a link, allowing other node handlers to process the click event.

packages/extension-link/__tests__/link.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Editor } from '@tiptap/core'
22
import Document from '@tiptap/extension-document'
3+
import Image from '@tiptap/extension-image'
34
import Link from '@tiptap/extension-link'
45
import Paragraph from '@tiptap/extension-paragraph'
56
import Text from '@tiptap/extension-text'
@@ -257,6 +258,56 @@ describe('extension-link', () => {
257258
})
258259
})
259260

261+
it('should return false when clicking on non-link elements', () => {
262+
editor = new Editor({
263+
element: createEditorEl(),
264+
extensions: [
265+
Document,
266+
Text,
267+
Paragraph,
268+
Image,
269+
Link.configure({
270+
openOnClick: false,
271+
}),
272+
],
273+
content: {
274+
type: 'doc',
275+
content: [
276+
{
277+
type: 'paragraph',
278+
content: [
279+
{
280+
type: 'image',
281+
attrs: {
282+
src: 'https://placehold.co/400',
283+
},
284+
},
285+
],
286+
},
287+
],
288+
},
289+
})
290+
291+
const editorEl = getEditorEl()
292+
const img = editorEl?.querySelector('img')
293+
294+
expect(img).toBeTruthy()
295+
296+
const clickEvent = new MouseEvent('click', {
297+
bubbles: true,
298+
cancelable: true,
299+
button: 0,
300+
})
301+
302+
const wasDefaultPrevented = !img?.dispatchEvent(clickEvent)
303+
304+
// The event should not be prevented by the link handler
305+
expect(wasDefaultPrevented).toBe(false)
306+
307+
editor?.destroy()
308+
getEditorEl()?.remove()
309+
})
310+
260311
describe('shouldAutoLink', () => {
261312
it('default shouldAutoLink rejects bare hostnames without TLD', () => {
262313
// Test using Link extension's default options

packages/extension-link/src/helpers/clickHandler.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,31 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
2323
return false
2424
}
2525

26+
let link: HTMLAnchorElement | null = null
27+
28+
if (event.target instanceof HTMLAnchorElement) {
29+
link = event.target
30+
} else {
31+
const target = event.target as HTMLElement | null
32+
if (!target) {
33+
return false
34+
}
35+
36+
const root = options.editor.view.dom
37+
38+
// Tntentionally limit the lookup to the editor root.
39+
// Using tag names like DIV as boundaries breaks with custom NodeViews,
40+
link = target.closest<HTMLAnchorElement>('a')
41+
42+
if (link && !root.contains(link)) {
43+
link = null
44+
}
45+
}
46+
47+
if (!link) {
48+
return false
49+
}
50+
2651
let handled = false
2752

2853
if (options.enableClickSelection) {
@@ -31,30 +56,11 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
3156
}
3257

3358
if (options.openOnClick) {
34-
let link: HTMLAnchorElement | null = null
35-
36-
if (event.target instanceof HTMLAnchorElement) {
37-
link = event.target
38-
} else {
39-
let a = event.target as HTMLElement
40-
const els = []
41-
42-
while (a.nodeName !== 'DIV') {
43-
els.push(a)
44-
a = a.parentNode as HTMLElement
45-
}
46-
link = els.find(value => value.nodeName === 'A') as HTMLAnchorElement
47-
}
48-
49-
if (!link) {
50-
return handled
51-
}
52-
5359
const attrs = getAttributes(view.state, options.type.name)
54-
const href = link?.href ?? attrs.href
55-
const target = link?.target ?? attrs.target
60+
const href = link.href ?? attrs.href
61+
const target = link.target ?? attrs.target
5662

57-
if (link && href) {
63+
if (href) {
5864
window.open(href, target)
5965
handled = true
6066
}

0 commit comments

Comments
 (0)