fix(layout-engine): render underlined tabs flush with text underline (SD-3330)#3611
Merged
harbournick merged 7 commits intoJun 3, 2026
Merged
Conversation
Underlined tab characters render their underline as a border-bottom on the tab box. The box was the full line height and bottom-aligned, so the border landed ~descent+half-leading below the text-decoration underline of adjacent text, making a continuous underline look broken where text meets tabs. Anchor the underlined tab box to the line-box top and end it at the baseline offset derived from the resolved line metrics (ascent/descent/lineHeight), so the border-bottom sits flush with the text underline. Gated to underlined tabs only; non-underlined tabs keep their previous geometry to avoid any tab-stop or line-layout regression. Covers SD-3347 signature/fill-in line rendering. No DOM measurement is added (SD-2957). Adds a regression test for both the inline and positioned paths.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Refactor the rendering of underlined tabs to use the same text-decoration mechanism as adjacent text, ensuring consistent baseline alignment and weight. This change addresses issues where the underline appeared misaligned due to the previous border-bottom approach. The tests have been updated to reflect these changes, ensuring that both underlined and plain tabs render correctly without unexpected borders. This aligns with the goal of maintaining visual fidelity across text and tab elements (SD-3330).
There was a problem hiding this comment.
cubic analysis
1 issue found across 3 files
Linked issue analysis
Linked issue: SD-3347: Feature: Render Word-style underlines
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Underlined tab characters render their underline flush with adjacent text underline (one continuous line matching Word). | PR changes tab rendering so underlined inline tabs use text-decoration on a baseline-aligned box and positioned tabs draw border-bottom at a computed baseline offset. This addresses the SD-3330 vertical offset and produces a continuous line. |
| ✅ | Fill-in and signature lines that appear as visible blank underlined lines in Word render as visible blank lines in SuperDoc (not ordinary text underlines). | PR gates behavior to underlined tabs and fills inline tab boxes with transparent whitespace so the underline is drawn via text-decoration, preserving blank-line appearance rather than collapsing to text underline. |
| ✅ | Behavior validated against targeted fixtures that reproduce tab-based and underline-based line patterns. | Author ran the change against the supplied fixtures listed in the PR and reports expected results for the key fixtures reproducing the issue. |
| ✅ | Preserve alignment, spacing, and tab-stop positioning (no layout regressions around tabs). | The change is scoped to underlined tabs only; positioned-tab path uses a border at computed offset so layout and positioning remain unchanged for non-underlined tabs. The author explicitly measured tab-stop positioning and noted no change. |
| ✅ | No regression to ordinary text underline rendering (text underlines remain correct and underline weight matches tabs). | text-run.ts now sets an explicit font-scaled textDecorationThickness and tab underline thickness reuses the same scaling, ensuring uniform weight across text and tab underlines; non-underlined tabs remain invisible. |
| ✅ | Regression test coverage added for the underlined-tab behavior. | A focused test file covering inline and positioned underlined and plain tabs was added and author reports RED on main → GREEN with fix. |
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…ht (SD-3330) The text-decoration approach for inline tab underlines required filling the tab with transparent whitespace for the browser to underline. That filler is selectable content, so selecting a line produced a broken/clipped selection highlight across the tab region, and it complicated the editor underline flow. Revert inline tabs to a border-bottom at the computed baseline offset (no filler, no selectable content, no selection artifacts). Keep the matched weight: the border width and text-decoration-thickness both use the shared font-scaled underlineThicknessPx, so text and tab underlines render at the same integer-px weight. Trade-off: the border position is a formula approximation of the text underline baseline (within ~1px) rather than the browser's exact placement, but it has no interaction side effects.
Applying underline to an already-rendered tab in the editor did not show until an unrelated edit forced a rebuild. Root cause: deriveBlockVersion (the paint cache key the DomPainter compares to decide whether to reuse a fragment) encoded a tab run as just text + "tab", omitting its marks. Toggling underline produced an identical version, so the painter reused the cached, non-underlined fragment. Include the tab's underline (style + color) in its version, matching how text runs already encode their underline. Now a tab mark change invalidates the paint cache and the underline appears immediately. Adds a regression test asserting the version changes when a tab gains/recolors an underline. Pre-existing; reproduced on main. Independent of the tab underline rendering fix.
…-3330) A line containing only tabs was measured at the 12px default and rendered ~4px shorter than a text or empty line in the same paragraph, so tab fill-in lines sat at a different height than typed text. Two parts: - Adapter (paragraph converter): a bare tab carries no font of its own, so give it the paragraph's resolved default font (mirroring the empty-paragraph run). - Measuring: when a paragraph has no sized text run, fall back to any run that carries a font size/family (e.g. a tab) instead of the 12px default, so the tab's font drives the line height. Add fontFamily/fontSize to the TabRun type (the tab legitimately carries them for line height and underline weight) and drop the matching casts. Tab widths are unaffected. Adds a measuring regression test asserting a tab-only line matches a text line of the same font. Pre-existing; independent of the underline work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Underlined tab characters now render their underline flush with the text underline, producing one continuous line as Word does. Fixes SD-3330 and delivers the core of SD-3347 (Word-style underlined fill-in / signature lines).
Linear: SD-3347 (parent feature), SD-3330 (child bug). Related: SD-1289, SD-1499, SD-3103.
The bug
In a DOCX where a run carries
<w:u w:val="single"/>and contains<w:tab/>, Word draws a single continuous underline under the text and the tab advance. SuperDoc drew the tab's underline ~8px below the text underline, so the line looked broken where text met tabs.Root cause: the tab underline is a
border-bottomon the tab box. The box was the full line height andvertical-align: bottom, so the border sat at the bottom of the line box (including leading) — not at the text baseline wheretext-decorationunderlines render. ECMA-376 §17.3.2.40 specifies the underline appears "directly below the character height (less all spacing above and below the characters on the line)", i.e. at the baseline, not the line-box bottom.The fix
painters/dom/src/runs/tab-run.ts: for underlined tabs, pin the box top to the line-box top and end it at a baseline offset derived from the resolved line metrics (ascent/descent/lineHeight), so theborder-bottomlands flush with the adjacent text underline. No DOM measurement is added (honors the painter's SD-2957 no-measure invariant).The change is gated to underlined tabs only — non-underlined tabs keep their previous geometry, so tab-stop positioning and nearby line layout are unchanged.
Validation (Layout Engine mode)
Measured the vertical step between the tab underline and the text underline on
underlined tab stops (1).docx: +8px → −1px (visually continuous).underlined tab stops (1)tab-underlinessd-1289-tabs-with-linesBy:/Name:/Title:lines flush with labels (were floating low) ✅tab-stops-test-signer-arearuns/tab-run.test.ts(inline + positioned paths): RED onmain, GREEN with the fix.@superdoc/painter-domsuite: 1183 passed.tsc -btypecheck clean.Notes / follow-ups
main:toggleUnderline, the toolbar, mark application, andisActive('underline')all handle tab nodes correctly. It appears to have been fixed since the reported v1.38.0; only the rendering remained.