From 21ff9872fce349070b222c756e66df94d120e54a Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Thu, 14 May 2026 07:17:46 -0300 Subject: [PATCH] fix(content-controls): default newly-created controls to richText, not unknown (SD-3139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to SD-3131 (#3275). After that PR, imported typeless w:sdtPr correctly resolves to controlType: 'richText' per ECMA-376 §17.5.2.26, but newly created controls via create.contentControl (no controlType) and contentControls.wrap still defaulted to 'unknown'. Customers filtering contentControls.list() by type saw different results before vs after save/reopen — the same typeless OOXML round-tripped from 'unknown' to 'richText'. - createWrapper: collapse three `?? 'unknown'` into a single `?? 'richText'` local. - wrapWrapper: explicitly seed controlType, type, and a default sdtPr with on the wrapper attrs (previously controlType was unset entirely, leaving resolveControlType to fall back to 'unknown'). Newly created controls now emit explicit in sdtPr for unambiguous engine state and exported markup. Imported typeless Word-authored SDTs preserve their original raw sdtPr (import path unchanged). Per the SD-3131 design, 'unknown' keeps its meaning of "unsupported or unrecognized type" — it's no longer the default for new controls. Tests: 3 new (default-create → richText, explicit-richText-create seeds , wrap → richText + seeds ). 42 wrappers + 1189 conformance + 1398 document-api pass. --- .../content-controls-wrappers.test.ts | 46 +++++++++++++++++++ .../plan-engine/content-controls-wrappers.ts | 21 +++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.test.ts index 9d6adad5a9..4a570dbe66 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.test.ts @@ -1095,6 +1095,52 @@ describe('create.contentControl default sdtPr seeding', () => { expect(dateEl?.elements?.some((el) => el.name === 'w:storeMappedDataAs')).toBe(true); expect(dateEl?.elements?.some((el) => el.name === 'w:calendar')).toBe(true); }); + + it('defaults newly-created controls without controlType to richText', () => { + const editor = makeSdtEditor(); + const adapter = createContentControlsAdapter(editor); + + const result = adapter.create({ kind: 'inline' }, { changeMode: 'direct' }); + expect(result.success).toBe(true); + + const insertInline = editor.commands!.insertStructuredContentInline as ReturnType; + const attrs = insertInline.mock.calls[0][0].attrs as Record; + expect(attrs.controlType).toBe('richText'); + expect(attrs.type).toBe('richText'); + const sdtPr = attrs.sdtPr as { elements?: Array<{ name: string }> }; + expect(sdtPr.elements?.some((el) => el.name === 'w:richText')).toBe(true); + }); + + it('seeds explicit richText controls with w:richText in sdtPr', () => { + const editor = makeSdtEditor(); + const adapter = createContentControlsAdapter(editor); + + const result = adapter.create({ kind: 'inline', controlType: 'richText' }, { changeMode: 'direct' }); + expect(result.success).toBe(true); + + const insertInline = editor.commands!.insertStructuredContentInline as ReturnType; + const attrs = insertInline.mock.calls[0][0].attrs as Record; + expect(attrs.controlType).toBe('richText'); + const sdtPr = attrs.sdtPr as { elements?: Array<{ name: string }> }; + expect(sdtPr.elements?.some((el) => el.name === 'w:richText')).toBe(true); + }); +}); + +describe('contentControls.wrap default classification', () => { + it('sets wrapper controlType to richText and seeds w:richText in sdtPr', () => { + const editor = makeSdtEditor(); + const adapter = createContentControlsAdapter(editor); + + adapter.wrap({ target: SDT_TARGET, kind: 'block' }, { changeMode: 'direct' }); + + const createFn = editor.schema.nodes.structuredContentBlock.create as ReturnType; + expect(createFn).toHaveBeenCalledTimes(1); + const [attrs] = createFn.mock.calls[0]; + expect(attrs.controlType).toBe('richText'); + expect(attrs.type).toBe('richText'); + const sdtPr = attrs.sdtPr as { elements?: Array<{ name: string }> }; + expect(sdtPr.elements?.some((el) => el.name === 'w:richText')).toBe(true); + }); }); describe('contentControls.setType default sdtPr seeding', () => { diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.ts index fb56ba41e1..311344ae24 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.ts @@ -493,8 +493,18 @@ function wrapWrapper( const nodeType = editor.schema.nodes[nodeTypeName]; if (!nodeType) return false; + // ECMA-376 §17.5.2.26: typeless sdtPr resolves to richText. Default here so + // the wrapper classifies the same in-session and after reimport. const wrapperNode = nodeType.create( - { id, tag: input.tag, alias: input.alias, lockMode: input.lockMode ?? 'unlocked' }, + { + id, + tag: input.tag, + alias: input.alias, + lockMode: input.lockMode ?? 'unlocked', + controlType: 'richText', + type: 'richText', + sdtPr: buildDefaultSdtPr('richText'), + }, resolved.node, ); const { tr } = editor.state; @@ -1809,15 +1819,18 @@ function createWrapper( } return executeSdtMutation(editor, target, options, () => { + // ECMA-376 §17.5.2.26: typeless sdtPr resolves to richText. 'unknown' is for + // unsupported/unrecognized type children, not a default for new controls. + const controlType = input.controlType ?? 'richText'; const attrs: Record = { id, tag: input.tag, alias: input.alias, lockMode: input.lockMode ?? 'unlocked', - controlType: input.controlType ?? 'unknown', - type: input.controlType ?? 'unknown', + controlType, + type: controlType, }; - const defaultSdtPr = buildDefaultSdtPr(input.controlType ?? 'unknown'); + const defaultSdtPr = buildDefaultSdtPr(controlType); const isDateCreate = input.controlType === 'date' && input.content == null; const dateDefaults = isDateCreate ? buildDateControlDefaults() : null; const sdtPrWithDateDefaults = dateDefaults ? applyDateDefaultsToSdtPr(defaultSdtPr, dateDefaults) : defaultSdtPr;