fix(runtime): <Transform> with no children renders no node, matching Ink (#105)

Ink's <Transform> returns null (no node) when children are undefined/null, and
that guard runs BEFORE the accessibilityLabel substitution (Transform.tsx:28-30).
vue always created a "transform" host node, so:
- an empty <Transform> in a flex `gap` row consumed a gap slot Ink never adds (P13); and
- a childless <Transform accessibilityLabel> emitted the label even though Ink's
  null guard wins over it (P19).

Add the null-children guard at the top of the render fn. Vue materializes a bare
null/false/undefined/v-if=false child as a single Comment vnode and cannot tell them
apart, so the predicate treats the whole group as "no children" (slot undefined OR
every vnode is a Comment) — matching Ink for the common `{null}`/`{cond ? x : null}`
idioms and keeping <Transform> consistent with vue-tui's documented comment-anchor
model (every other component already omits a false/v-if child). An empty-string ({''},
a Text vnode) or JSX empty array ({[]}, a Fragment) still renders, matching Ink.

This deliberately diverges from Ink only for a literal {false} / {cond && x}-false
child (React's false !== null → Ink renders an empty gap-slot node); documented in
ink-divergences.md and locked by a test, since Vue physically cannot distinguish it
from null.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-01 00:29:21 +08:00
committed by GitHub
parent 6078cb7a80
commit 224afeb3a2
3 changed files with 164 additions and 3 deletions
+8 -1
View File
@@ -145,7 +145,14 @@ built never reaches the terminal:
- **A `v-if=false` branch (or a `null`/`false`/`undefined` child) leaves a comment anchor
(`TuiComment`)** where Ink emits no node, but it is inert: no yoga node, paints nothing,
never shifts a sibling's yoga index, and is skipped for the positional `<Transform>` index
in all three squash paths (`G52`). Output equals omitting the element.
in all three squash paths (`G52`). Output equals omitting the element. This also governs
`<Transform>`'s own children guard: a childless `<Transform>` (or one whose only child is a
`null`/`false`/`v-if=false` comment anchor) renders **no node** (matching Ink for `null`,
consistent with every other component). It diverges from Ink only for a literal `{false}` /
`{cond && x}`-false child — React's `false !== null`, so Ink renders an empty node (and a gap
slot); Vue collapses `false`/`null` to the same `TuiComment` and cannot distinguish them, so
it omits the node. Keeping `<Transform>` consistent with the comment-anchor model is the
principled choice.
- **Commit timing is deliberately Ink-aligned** — leading+trailing throttle at
`ceil(1000/maxFps)` ≈ 32 ms (Ink's `renderThrottleMs`), synchronous resize — even though
re-renders are Vue's fine-grained reactivity, not a React subtree re-render.