diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts index fb5d17840b..b784b1b5fe 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts @@ -101,6 +101,11 @@ function isDirectSingleCommentHighlightHit(target: EventTarget | null): boolean return getCommentHighlightThreadIds(target).length === 1; } +function isDirectTrackedChangeHit(target: EventTarget | null): boolean { + if (!(target instanceof Element)) return false; + return target.closest(TRACK_CHANGE_SELECTOR) != null; +} + function resolveTrackChangeThreadId(target: EventTarget | null): string | null { if (!(target instanceof Element)) { return null; @@ -220,11 +225,11 @@ function shouldIgnoreRepeatClickOnActiveComment( return false; } - // Direct clicks on commented text should place a caret at the clicked - // position and let the comments plugin infer the active thread from the - // resulting selection. Only preserve the pointerdown short-circuit for - // nearby non-text surfaces, such as split-run gaps. - if (isDirectSingleCommentHighlightHit(target)) { + // Direct clicks on single-thread comment text or tracked-change text should + // place a caret at the clicked position and let comment/thread activation be + // inferred from the resulting selection. Only preserve the pointerdown + // short-circuit for nearby non-text surfaces, such as split-run gaps. + if (isDirectSingleCommentHighlightHit(target) || isDirectTrackedChangeHit(target)) { return false; } @@ -2274,7 +2279,9 @@ export class EditorInputManager { } #handleSingleCommentHighlightClick(event: PointerEvent, target: HTMLElement | null, editor: Editor): boolean { - if (isDirectSingleCommentHighlightHit(target)) { + // Direct hits on inline annotated text should not be intercepted here. + // Let generic click-to-position place the caret at the clicked pixel. + if (isDirectSingleCommentHighlightHit(target) || isDirectTrackedChangeHit(target)) { return false; } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.activeCommentClick.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.activeCommentClick.test.ts index ca2d06e4db..a312d39902 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.activeCommentClick.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.activeCommentClick.test.ts @@ -238,7 +238,7 @@ describe('EditorInputManager - single-thread comment highlight clicks', () => { expect(viewportHost.setPointerCapture).toHaveBeenCalled(); }); - it('activates a tracked-change decoration when it owns the clicked visual surface', () => { + it('lets direct clicks on a tracked-change decoration fall through to generic caret placement', () => { mockEditor.state.comments$.activeThreadId = 'comment-2'; const trackedChange = document.createElement('span'); @@ -248,12 +248,32 @@ describe('EditorInputManager - single-thread comment highlight clicks', () => { dispatchPointerDown(trackedChange); - expect(mockEditor.commands.setCursorById).toHaveBeenCalledWith('change-1', { - activeCommentId: 'change-1', - }); - expect(resolvePointerPositionHit).not.toHaveBeenCalled(); - expect(mockEditor.state.tr.setSelection).not.toHaveBeenCalled(); - expect(viewportHost.setPointerCapture).not.toHaveBeenCalled(); + expect(mockEditor.commands.setCursorById).not.toHaveBeenCalled(); + expect(resolvePointerPositionHit).toHaveBeenCalled(); + expect(TextSelection.create as unknown as Mock).toHaveBeenCalled(); + expect(mockEditor.state.tr.setSelection).toHaveBeenCalled(); + expect(viewportHost.setPointerCapture).toHaveBeenCalled(); + }); + + it('lets repeat direct clicks on the active tracked-change decoration reposition caret', () => { + mockEditor.state.comments$.activeThreadId = 'change-1'; + + const trackedChange = document.createElement('span'); + trackedChange.className = 'track-delete-dec highlighted'; + trackedChange.setAttribute('data-track-change-id', 'change-1'); + viewportHost.appendChild(trackedChange); + + dispatchPointerDown(trackedChange); + + expect(mockEditor.emit).not.toHaveBeenCalledWith( + 'commentsUpdate', + expect.objectContaining({ activeCommentId: 'change-1' }), + ); + expect(mockEditor.commands.setCursorById).not.toHaveBeenCalled(); + expect(resolvePointerPositionHit).toHaveBeenCalled(); + expect(TextSelection.create as unknown as Mock).toHaveBeenCalled(); + expect(mockEditor.state.tr.setSelection).toHaveBeenCalled(); + expect(viewportHost.setPointerCapture).toHaveBeenCalled(); }); it('activates a nearby single-thread highlight when a split-run gap receives the pointer event', () => {