From 5cf3663804e0f8e6a5a4196eafbd71ab4427bfe9 Mon Sep 17 00:00:00 2001 From: Artem Nistuley Date: Mon, 27 Apr 2026 17:29:41 +0300 Subject: [PATCH] fix: sdt field style --- .../painters/dom/src/index.test.ts | 179 ++++++++++++++++++ .../painters/dom/src/renderer.ts | 15 ++ 2 files changed, 194 insertions(+) diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index 0a1192a457..bb3ceed488 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -2345,6 +2345,185 @@ describe('DomPainter', () => { expect(wrapper.textContent).toContain('controlled text'); }); + it('keeps inline SDT wrapper font-size in sync when run font-size changes', () => { + const block: FlowBlock = { + kind: 'paragraph', + id: 'inline-sdt-font-sync', + runs: [ + { + text: 'Company Address', + fontFamily: 'Arial, sans-serif', + fontSize: 14.6667, + pmStart: 2044, + pmEnd: 2059, + sdt: { + type: 'structuredContent', + scope: 'inline', + id: '574416526', + tag: 'inline_text_sdt', + alias: 'Company Address', + lockMode: 'unlocked', + }, + }, + ], + attrs: {}, + }; + + const measure: Measure = { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: 15, + width: 180, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + ], + totalHeight: 20, + }; + + const layout: Layout = { + pageSize: { w: 612, h: 792 }, + pages: [ + { + number: 1, + fragments: [ + { + kind: 'para', + blockId: 'inline-sdt-font-sync', + fromLine: 0, + toLine: 1, + x: 30, + y: 40, + width: 552, + pmStart: 2044, + pmEnd: 2059, + }, + ], + }, + ], + }; + + const painter = createTestPainter({ blocks: [block], measures: [measure] }); + painter.paint(layout, mount); + + const getInlineSdtWrapper = () => + mount.querySelector('.superdoc-structured-content-inline[data-sdt-id="574416526"]') as HTMLElement | null; + + const wrapperBefore = getInlineSdtWrapper(); + expect(wrapperBefore).toBeTruthy(); + expect(wrapperBefore?.style.fontSize).toBe('14.6667px'); + + const updatedBlock: FlowBlock = { + ...block, + runs: [ + { + ...block.runs[0], + fontSize: 10, + }, + ], + }; + + painter.setData([updatedBlock], [measure]); + painter.paint(layout, mount); + + const wrapperAfter = getInlineSdtWrapper(); + expect(wrapperAfter).toBeTruthy(); + expect(wrapperAfter?.style.fontSize).toBe('10px'); + }); + + it('uses first run font-size for inline SDT wrapper when a field has mixed run sizes', () => { + const mixedSizeBlock: FlowBlock = { + kind: 'paragraph', + id: 'inline-sdt-mixed-font-size', + runs: [ + { + text: 'Big', + fontFamily: 'Arial, sans-serif', + fontSize: 36, + pmStart: 100, + pmEnd: 103, + sdt: { + type: 'structuredContent', + scope: 'inline', + id: 'mixed-size-sdt', + tag: 'inline_text_sdt', + alias: 'Mixed Size Field', + lockMode: 'unlocked', + }, + }, + { + text: ' small', + fontFamily: 'Arial, sans-serif', + fontSize: 10, + pmStart: 103, + pmEnd: 109, + sdt: { + type: 'structuredContent', + scope: 'inline', + id: 'mixed-size-sdt', + tag: 'inline_text_sdt', + alias: 'Mixed Size Field', + lockMode: 'unlocked', + }, + }, + ], + attrs: {}, + }; + + const mixedSizeMeasure: Measure = { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 1, + toChar: 6, + width: 180, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + ], + totalHeight: 20, + }; + + const mixedSizeLayout: Layout = { + pageSize: { w: 612, h: 792 }, + pages: [ + { + number: 1, + fragments: [ + { + kind: 'para', + blockId: 'inline-sdt-mixed-font-size', + fromLine: 0, + toLine: 1, + x: 30, + y: 40, + width: 552, + pmStart: 100, + pmEnd: 109, + }, + ], + }, + ], + }; + + const painter = createTestPainter({ blocks: [mixedSizeBlock], measures: [mixedSizeMeasure] }); + painter.paint(mixedSizeLayout, mount); + + const wrapper = mount.querySelector( + '.superdoc-structured-content-inline[data-sdt-id="mixed-size-sdt"]', + ) as HTMLElement | null; + expect(wrapper).toBeTruthy(); + expect(wrapper?.style.fontSize).toBe('36px'); + }); + it('positions word-layout markers relative to the text start', () => { const markerBlock: FlowBlock = { kind: 'paragraph', diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index dc933d12d2..5abf7eba1b 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -6364,6 +6364,7 @@ export class DomPainter { geoSdtWrapper.style.top = '0px'; geoSdtWrapper.style.height = `${line.lineHeight}px`; } + this.syncInlineSdtWrapperTypography(geoSdtWrapper, runForSdt); elem.style.left = `${elemLeftPx - geoSdtWrapperLeft}px`; geoSdtMaxRight = Math.max(geoSdtMaxRight, elemLeftPx + elemWidthPx); this.expandSdtWrapperPmRange(geoSdtWrapper, (runForSdt as TextRun).pmStart, (runForSdt as TextRun).pmEnd); @@ -6651,8 +6652,11 @@ export class DomPainter { if (resolved && this.doc) { if (!currentInlineSdtWrapper) { currentInlineSdtWrapper = this.createInlineSdtWrapper(resolved.sdt); + this.syncInlineSdtWrapperTypography(currentInlineSdtWrapper, run); currentInlineSdtId = runSdtId; } + // Typography is set when wrapper is created from the first run. + // Follow-up (SD-2744): define a deterministic mixed-typography rule. this.expandSdtWrapperPmRange(currentInlineSdtWrapper, run.pmStart, run.pmEnd); currentInlineSdtWrapper.appendChild(elem); } else { @@ -7053,6 +7057,17 @@ export class DomPainter { return wrapper; } + private syncInlineSdtWrapperTypography(wrapper: HTMLElement, runForSizing?: Run): void { + // The line container sets fontSize:0 (strut fix). Keep wrapper typography + // synced with the current run so border height tracks text-size edits. + const runFontSize = + runForSizing && 'fontSize' in runForSizing && typeof runForSizing.fontSize === 'number' + ? `${runForSizing.fontSize}px` + : BROWSER_DEFAULT_FONT_SIZE; + wrapper.style.fontSize = runFontSize; + wrapper.style.lineHeight = 'normal'; + } + /** * Expand the PM position range tracked on an SDT wrapper to include a new run's range. */