From 3e95183c47bede6d9d6e301ff44466ce24371a2c Mon Sep 17 00:00:00 2001 From: arimieandreea Date: Tue, 21 Jul 2026 12:47:27 +0300 Subject: [PATCH 1/2] fix(editable-html-tip-tap): gets a normal TextSelection after insertion PIE-581 --- .../editable-html-tip-tap/src/extensions/responseArea.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/editable-html-tip-tap/src/extensions/responseArea.js b/packages/editable-html-tip-tap/src/extensions/responseArea.js index 29c722cd6..95d191385 100644 --- a/packages/editable-html-tip-tap/src/extensions/responseArea.js +++ b/packages/editable-html-tip-tap/src/extensions/responseArea.js @@ -195,7 +195,13 @@ export const ResponseAreaExtension = Extension.create({ // tr.setSelection(NodeSelection.create(tr.doc, usedPos)) // --- Cursor move behavior for certain types (Slate: moveFocusTo next text) --- - if (['math_templated', 'inline_dropdown', 'explicit_constructed_response'].includes(typeName)) { + // Only types whose node view opens its own toolbar on selection + // (inline_dropdown, explicit_constructed_response) benefit from a + // NodeSelection here — the toolbar is the visual feedback that it's + // selected. math_templated has no such UI, so a NodeSelection there + // just looks unfocused (a highlighted node, no blinking caret); + // moving the cursor to just after it gives a real, visible caret. + if (['inline_dropdown', 'explicit_constructed_response'].includes(typeName)) { tr.setSelection(NodeSelection.create(tr.doc, usedPos)); } else { const after = usedPos + newInline.nodeSize; From 47a8f89717eb1acedcbbddfe618f4f040a1021d5 Mon Sep 17 00:00:00 2001 From: arimieandreea Date: Tue, 21 Jul 2026 15:19:29 +0300 Subject: [PATCH 2/2] fix: add tests for handling node insertion PIE-581 --- .../extensions/__tests__/responseArea.test.js | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js b/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js index b1f019c08..26279d0cb 100644 --- a/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js +++ b/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js @@ -264,16 +264,16 @@ describe('ResponseAreaExtension', () => { jest.resetModules(); }); - const buildInsertCommand = () => { + const buildInsertCommand = (type = 'inline-dropdown') => { const { ResponseAreaExtension } = require('../responseArea'); const context = { options: { - type: 'inline-dropdown', + type, maxResponseAreas: 5, }, }; const commands = ResponseAreaExtension.addCommands.call(context); - return commands.insertResponseArea('inline-dropdown'); + return commands.insertResponseArea(type); }; const createDoc = (existingCount, typeName = 'inline_dropdown') => ({ @@ -391,6 +391,60 @@ describe('ResponseAreaExtension', () => { expect(create).not.toHaveBeenCalled(); expect(mockTr.insert).not.toHaveBeenCalled(); }); + + describe('selection after insert', () => { + const buildStateAndTr = (typeName, { withResolve = false } = {}) => { + const mockInlineNode = { nodeSize: 1 }; + const create = jest.fn(() => mockInlineNode); + const mockDoc = { + descendants: jest.fn(), + content: { size: 50 }, + ...(withResolve ? { resolve: jest.fn((pos) => ({ resolved: pos })) } : {}), + }; + const mockTr = { + insert: jest.fn(), + doc: mockDoc, + setSelection: jest.fn(), + }; + const state = { + schema: { nodes: { [typeName]: { create } } }, + doc: mockDoc, + selection: { from: 5 }, + }; + + return { mockTr, state }; + }; + + it.each(['inline-dropdown', 'explicit-constructed-response'])( + 'sets a NodeSelection on the inserted node for %s', + (type) => { + const insert = buildInsertCommand(type); + const typeName = type.replace(/-/g, '_'); + const { mockTr, state } = buildStateAndTr(typeName); + const { NodeSelection, TextSelection } = require('prosemirror-state'); + + insert({ tr: mockTr, state, dispatch: jest.fn(), commands: { focus: jest.fn() } }); + + expect(NodeSelection.create).toHaveBeenCalledWith(mockTr.doc, 5); + expect(TextSelection.near).not.toHaveBeenCalled(); + expect(mockTr.setSelection).toHaveBeenCalledWith({ type: 'node', pos: 5 }); + }, + ); + + it('sets a TextSelection just after the inserted node for math-templated (no NodeSelection)', () => { + const insert = buildInsertCommand('math-templated'); + const { mockTr, state } = buildStateAndTr('math_templated', { withResolve: true }); + const { NodeSelection, TextSelection } = require('prosemirror-state'); + + insert({ tr: mockTr, state, dispatch: jest.fn(), commands: { focus: jest.fn() } }); + + expect(NodeSelection.create).not.toHaveBeenCalled(); + // usedPos (5) + nodeSize (1) === 6 + expect(mockTr.doc.resolve).toHaveBeenCalledWith(6); + expect(TextSelection.near).toHaveBeenCalledWith({ resolved: 6 }, 1); + expect(mockTr.setSelection).toHaveBeenCalledWith({ type: 'text', pos: { resolved: 6 }, dir: 1 }); + }); + }); }); }); });