@miaskiewicz/turbo-dom 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -23,6 +23,43 @@ const SHORTHAND_OF = {
23
23
  'padding-top': 'padding', 'padding-right': 'padding', 'padding-bottom': 'padding', 'padding-left': 'padding',
24
24
  };
25
25
 
26
+ // Length properties whose bare `0` real browsers serialize as `0px` in computed style.
27
+ const LENGTH_PROPS = new Set([
28
+ 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height',
29
+ 'top', 'right', 'bottom', 'left', 'flex-basis', 'gap', 'row-gap', 'column-gap',
30
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
31
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
32
+ 'border-width', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
33
+ 'font-size', 'letter-spacing', 'word-spacing', 'text-indent',
34
+ ]);
35
+
36
+ const BORDER_STYLES = new Set(['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']);
37
+
38
+ // Expand the common shorthands into longhands so longhand computed getters resolve
39
+ // (e.g. `border:1px solid` → borderWidth '1px'). Cheap, test-time only; later
40
+ // declarations of the same longhand override naturally (cascade order preserved).
41
+ function setProp(map, name, val) {
42
+ map.set(name, val);
43
+ if (name === 'margin' || name === 'padding') {
44
+ const t = val.trim().split(/\s+/);
45
+ const top = t[0], right = t[1] ?? top, bottom = t[2] ?? top, left = t[3] ?? right;
46
+ map.set(name + '-top', top); map.set(name + '-right', right);
47
+ map.set(name + '-bottom', bottom); map.set(name + '-left', left);
48
+ } else if (name === 'border') {
49
+ let width, style, color;
50
+ for (const p of val.trim().split(/\s+/)) {
51
+ if (BORDER_STYLES.has(p)) style = p;
52
+ else if (/^(thin|medium|thick)$/.test(p) || /^[\d.]+(px|em|rem|%|pt|vh|vw)?$/.test(p)) width = p;
53
+ else color = p;
54
+ }
55
+ if (width !== undefined) { map.set('border-width', width); for (const s of ['top', 'right', 'bottom', 'left']) map.set(`border-${s}-width`, width); }
56
+ if (style !== undefined) map.set('border-style', style);
57
+ if (color !== undefined) map.set('border-color', color);
58
+ } else if (name === 'background' && !/\s/.test(val.trim())) {
59
+ map.set('background-color', val.trim());
60
+ }
61
+ }
62
+
26
63
  function parseDecls(text, into) {
27
64
  const map = into || new Map();
28
65
  for (const decl of text.split(';')) {
@@ -30,7 +67,7 @@ function parseDecls(text, into) {
30
67
  if (c === -1) continue;
31
68
  const name = decl.slice(0, c).trim().toLowerCase();
32
69
  if (!name) continue;
33
- map.set(name, decl.slice(c + 1).trim().replace(/\s*!\s*important\s*$/i, ''));
70
+ setProp(map, name, decl.slice(c + 1).trim().replace(/\s*!\s*important\s*$/i, ''));
34
71
  }
35
72
  return map;
36
73
  }
@@ -101,11 +138,15 @@ function resolve(el, rules) {
101
138
  }
102
139
 
103
140
  function lookup(map, prop) {
104
- const direct = map.get(prop);
105
- if (direct !== undefined) return direct;
106
- const sh = SHORTHAND_OF[prop];
107
- if (sh) { const v = map.get(sh); if (v !== undefined && !/\s/.test(v.trim())) return v; }
108
- return '';
141
+ let v = map.get(prop);
142
+ if (v === undefined) {
143
+ const sh = SHORTHAND_OF[prop];
144
+ if (sh) { const s = map.get(sh); if (s !== undefined && !/\s/.test(s.trim())) v = s; }
145
+ }
146
+ if (v === undefined) return '';
147
+ // px-normalize a bare `0` for length properties (browsers report `0px`)
148
+ if (v === '0' && LENGTH_PROPS.has(prop)) return '0px';
149
+ return v;
109
150
  }
110
151
 
111
152
  function makeProxy(map, v) {
@@ -461,13 +461,13 @@ export class Element extends Node {
461
461
  const t = this.localName;
462
462
  if (t === 'select') { for (const o of this.getElementsByTagName('option')) o.selected = (o.value === String(x)); return; }
463
463
  if (t === 'option') { this.setAttribute('value', x); return; }
464
- // typed <input> sanitizes per the value-sanitization algorithm: invalid
465
- // values for date/time/number/etc are rejected (value left unchanged), so
466
- // user-event typing them char-by-char doesn't emit bogus partial values.
464
+ // typed <input> runs the WHATWG value-sanitization algorithm: a value that
465
+ // isn't valid for date/time/number/etc becomes the EMPTY string (not the prior
466
+ // value). Matches real browsers — React's value tracker then sees the change and
467
+ // fires onChange, where retaining the old value would silently swallow it.
467
468
  if (t === 'input') {
468
469
  const sanitized = sanitizeInputValue((this.getAttribute('type') || 'text').toLowerCase(), String(x));
469
- if (sanitized === null) return; // invalid → reject, keep current value
470
- this.__value = sanitized;
470
+ this.__value = sanitized === null ? '' : sanitized;
471
471
  } else {
472
472
  this.__value = String(x);
473
473
  }