Skip to content

Commit 33b81eb

Browse files
committed
fix(#429): don't freeze auto-sized table cell widths
Table cells get their used width from the table layout algorithm, not from CSS. I was freezing that fractional used width (e.g. width:113.484px) as an explicit width on every <td>/<th>, which pins the auto-sized cell. At dpr=2 / under different font metrics in the clone the content no longer fits the frozen width and wraps to a new line (the "✅ 2024-09-16" case). I now skip emitting width on table cells — same rationale as the existing inline-sized-tag skip — so the cloned table sizes its cells just like the live one. Widths set via the width attribute or <col> still survive (they're attributes, not computed CSS).
1 parent 880376b commit 33b81eb

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// #429 — table cells get their used width from the table layout algorithm. snapdom used to
2+
// freeze that fractional used width (e.g. width:113.484px) as an explicit CSS width on each
3+
// <td>, which pins the auto-sized cell; at dpr=2 / under different font metrics the content no
4+
// longer fits the frozen width and the text wraps to a new line. The fix: don't emit width on
5+
// auto-sized table cells, so the cloned table sizes them just like the live one.
6+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
7+
import { snapdom } from '../src/index'
8+
9+
function svgOf(raw) {
10+
return decodeURIComponent(raw.replace(/^data:image\/svg\+xml;charset=utf-8,/, ''))
11+
}
12+
function classRules(svg) {
13+
const css = (svg.match(/<style[^>]*>([\s\S]*?)<\/style>/) || [])[1] || ''
14+
const rules = {}
15+
css.replace(/\.(c\d+)\s*\{([^}]*)\}/g, (_, n, b) => { rules[n] = b; return _ })
16+
return rules
17+
}
18+
19+
describe('#429 table cell width is not frozen', () => {
20+
let host
21+
beforeEach(() => { host = document.createElement('div'); document.body.appendChild(host) })
22+
afterEach(() => host.remove())
23+
24+
it('does not emit an explicit width on auto-sized <td>', async () => {
25+
host.innerHTML = `
26+
<table><tbody>
27+
<tr><td>Case 1:</td><td>some longer cell content 2024-09-16</td></tr>
28+
<tr><td>Case 2:</td><td>another cell value here</td></tr>
29+
</tbody></table>`
30+
const raw = await snapdom.toRaw(host.querySelector('table'))
31+
const svg = svgOf(raw)
32+
const rules = classRules(svg)
33+
const tds = svg.match(/<td[^>]*>/g) || []
34+
expect(tds.length).toBe(4)
35+
for (const tag of tds) {
36+
// no inline width frozen on the tag
37+
const inline = (tag.match(/style="([^"]*)"/) || [])[1] || ''
38+
expect(/(?:^|;)\s*(min-|max-)?width\s*:/.test(inline)).toBe(false)
39+
// no width in the generated class either
40+
const cls = (tag.match(/class="([^"]*)"/) || [])[1] || ''
41+
for (const c of cls.split(/\s+/).filter(Boolean)) {
42+
const body = rules[c] || ''
43+
expect(/(?:^|;)\s*(min-|max-)?width\s*:/.test(body)).toBe(false)
44+
}
45+
}
46+
})
47+
})

src/utils/css.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ export function getStyleKey(snapshot, tagName) {
156156
// Tags that size to text content; grid/flex blockify them but we should not constrain
157157
// width (causes wrap when font-weight makes text wider than captured width, e.g. "Timestamp demo")
158158
const INLINE_SIZED_TAGS = new Set(['span', 'small', 'em', 'strong', 'b', 'i', 'u', 's', 'code', 'cite', 'mark', 'sub', 'sup'])
159-
const skipWidth = isInline || INLINE_SIZED_TAGS.has(tagName)
159+
// #429: table cells get their used width from the table layout algorithm, not from CSS.
160+
// getComputedStyle reports that fractional used width (e.g. 113.484px); freezing it as an
161+
// explicit width pins the auto-sized cell, so any sub-pixel rendering difference in the clone
162+
// (dpr=2 rounding, font metrics) no longer fits and the text wraps. Let the table size cells.
163+
const TABLE_CELL_TAGS = new Set(['td', 'th'])
164+
const skipWidth = isInline || INLINE_SIZED_TAGS.has(tagName) || TABLE_CELL_TAGS.has(tagName)
160165
for (let [prop, value] of Object.entries(snapshot)) {
161166
if (shouldIgnoreProp(prop)) continue
162167
if (skipWidth && (prop === 'width' || prop === 'min-width' || prop === 'max-width')) continue

0 commit comments

Comments
 (0)