fix(runtime): backgroundColor="" on a Text opts out of the inherited Box bg (Ink parity) (#83)
An explicit backgroundColor="" on a descendant Text now renders bare glyphs instead of bleeding the inherited Box background. Mirrors Ink Text.tsx:103-106 (effectiveBg = backgroundColor ?? inheritedBg; colorize only when truthy): undefined inherits, "" opts out. The trailing padding still uses the inherited Box bg (Box fill), so glyphs carry their effective bg while the Box fills the rest -- byte-identical to Ink. Corrects the background-color.test.tsx mixed snapshot, which had encoded the buggy green-bleed output. Adds the opt-out test. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,24 @@ test("Multiple Text elements inherit same background", async ({ expect }) => {
|
|||||||
expect(lastFrame()).toMatchInlineSnapshot(`"[43mHello World[49m"`);
|
expect(lastFrame()).toMatchInlineSnapshot(`"[43mHello World[49m"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ink parity (Text.tsx:103-106): a child <Text>'s effective background is
|
||||||
|
// `backgroundColor ?? inheritedBackgroundColor`, and the bg wrap is applied only
|
||||||
|
// when that value is truthy. An explicit `backgroundColor=""` is NOT undefined,
|
||||||
|
// so it does NOT inherit — it resolves to `""` (falsy) and OPTS OUT of the
|
||||||
|
// inherited Box background, rendering the glyphs with no bg.
|
||||||
|
test("Text backgroundColor='' opts out of inherited Box background", async ({ expect }) => {
|
||||||
|
const { lastFrame } = await render(
|
||||||
|
defineComponent(() => () => (
|
||||||
|
<Box backgroundColor="green" alignSelf="flex-start">
|
||||||
|
<Text backgroundColor="">No BG</Text>
|
||||||
|
</Box>
|
||||||
|
)),
|
||||||
|
{ columns: 100 },
|
||||||
|
);
|
||||||
|
// Bare text, no green (42) bleed and no bg reset (49) at all.
|
||||||
|
expect(lastFrame()).toBe("No BG");
|
||||||
|
});
|
||||||
|
|
||||||
test("Mixed text with and without background inheritance", async ({ expect }) => {
|
test("Mixed text with and without background inheritance", async ({ expect }) => {
|
||||||
const { lastFrame } = await render(
|
const { lastFrame } = await render(
|
||||||
defineComponent(() => () => (
|
defineComponent(() => () => (
|
||||||
@@ -157,7 +175,9 @@ test("Mixed text with and without background inheritance", async ({ expect }) =>
|
|||||||
)),
|
)),
|
||||||
{ columns: 100 },
|
{ columns: 100 },
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchInlineSnapshot(`"[42mInherited No BG [41mRed BG[49m"`);
|
// Matches Ink background.tsx:106-116: bgGreen('Inherited ') + 'No BG ' + bgRed('Red BG').
|
||||||
|
// The "" Text opts out, so green is reset (49) before "No BG " and it renders bare.
|
||||||
|
expect(lastFrame()).toMatchInlineSnapshot(`"[42mInherited [49mNo BG [41mRed BG[49m"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Complex nested structure with background inheritance", async ({ expect }) => {
|
test("Complex nested structure with background inheritance", async ({ expect }) => {
|
||||||
|
|||||||
@@ -553,18 +553,37 @@ function paintNode(
|
|||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
const layout = node.yoga.getComputedLayout();
|
const layout = node.yoga.getComputedLayout();
|
||||||
const bgProps: TextProps = inheritedBg ? { backgroundColor: inheritedBg } : {};
|
// Ink Text.tsx:103-106: a Text's effective background is its OWN
|
||||||
|
// backgroundColor if defined (`??`, so an explicit "" is honored), else the
|
||||||
|
// inherited Box background; the bg is applied only when truthy. Passing the
|
||||||
|
// effective value as the squash base means an explicit "" opts OUT (renders
|
||||||
|
// bare glyphs) while `undefined` inherits — matching Ink's `??` semantics.
|
||||||
|
// (renderTextWithInlineStyles still lets the node's own props override this
|
||||||
|
// base, so the result is identical, but stating the effective value here
|
||||||
|
// keeps the intent explicit.)
|
||||||
|
const effectiveBg = (node.props["backgroundColor"] as string | undefined) ?? inheritedBg;
|
||||||
|
const bgProps: TextProps = effectiveBg ? { backgroundColor: effectiveBg } : {};
|
||||||
const text = renderTextWithInlineStyles(node, bgProps);
|
const text = renderTextWithInlineStyles(node, bgProps);
|
||||||
// Skip writing empty text — avoids applying line transformers to empty
|
// Skip writing empty text — avoids applying line transformers to empty
|
||||||
// content, which matches Ink's behavior of not writing empty text nodes.
|
// content, which matches Ink's behavior of not writing empty text nodes.
|
||||||
if (text === "") return;
|
if (text === "") return;
|
||||||
const cellWidth = Math.max(1, Math.floor(layout.width));
|
const cellWidth = Math.max(1, Math.floor(layout.width));
|
||||||
const wrapped = wrapText(text, cellWidth, node.props.wrap ?? "wrap");
|
const wrapped = wrapText(text, cellWidth, node.props.wrap ?? "wrap");
|
||||||
|
// Pad each line to the cell width with the INHERITED Box background only —
|
||||||
|
// this fills the space behind the text with the Box's bg (the Box also fills
|
||||||
|
// it via fillBackground), and is the reason a Box bg pads to full width while
|
||||||
|
// a text-only bg does not. The padding uses `inheritedBg`, NOT the effective
|
||||||
|
// bg: a Text that overrides or opts out (backgroundColor / "") only recolors
|
||||||
|
// its OWN glyphs, never the surrounding Box fill. The already-rendered glyphs
|
||||||
|
// in `wrapped[i]` keep their effective bg, so a `backgroundColor=""` Text
|
||||||
|
// stays bare even though we pad the trailing cells with the inherited bg.
|
||||||
if (inheritedBg) {
|
if (inheritedBg) {
|
||||||
|
const padProps: TextProps = { backgroundColor: inheritedBg };
|
||||||
for (let i = 0; i < wrapped.length; i++) {
|
for (let i = 0; i < wrapped.length; i++) {
|
||||||
const pad = cellWidth - stringWidth(wrapped[i]!);
|
const pad = cellWidth - stringWidth(wrapped[i]!);
|
||||||
const padStr = pad > 0 ? " ".repeat(pad) : "";
|
if (pad > 0) {
|
||||||
wrapped[i] = applyChalk(wrapped[i]! + padStr, bgProps);
|
wrapped[i] = wrapped[i]! + applyChalk(" ".repeat(pad), padProps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output.write(x0 + layout.left, y0 + layout.top, wrapped, transformers);
|
output.write(x0 + layout.left, y0 + layout.top, wrapped, transformers);
|
||||||
|
|||||||
Reference in New Issue
Block a user