fix: Newline renders as yoga carrier (text node) when standalone
Match Ink's behavior where Newline renders as ink-text (yoga carrier). When inside a Text parent, Newline still renders as virtual-text for inline behavior. When standalone, it renders as a text node so it participates in yoga layout and occupies vertical space. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { expect, test } from "vite-plus/test";
|
||||||
|
import { render } from "@vue-tui/testing";
|
||||||
|
import { Box, Text, Newline } from "@vue-tui/runtime";
|
||||||
|
|
||||||
|
test("Newline works standalone (outside Text) as a yoga carrier", async () => {
|
||||||
|
// In Ink, Newline renders as ink-text (yoga carrier) so it works
|
||||||
|
// outside a Text context as a standalone line break.
|
||||||
|
const { lastFrame } = await render(
|
||||||
|
defineComponent(() => () => (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Newline />
|
||||||
|
<Text>hi</Text>
|
||||||
|
</Box>
|
||||||
|
)),
|
||||||
|
{ columns: 100 },
|
||||||
|
);
|
||||||
|
// Newline should occupy a line before "hi"
|
||||||
|
const frame = lastFrame({ trimLines: true })!;
|
||||||
|
const lines = frame.split("\n");
|
||||||
|
expect(lines.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(lines.at(-1)).toBe("hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Newline count=2 adds two blank lines standalone", async () => {
|
||||||
|
const { lastFrame } = await render(
|
||||||
|
defineComponent(() => () => (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text>above</Text>
|
||||||
|
<Newline count={2} />
|
||||||
|
<Text>below</Text>
|
||||||
|
</Box>
|
||||||
|
)),
|
||||||
|
{ columns: 100 },
|
||||||
|
);
|
||||||
|
const frame = lastFrame({ trimLines: true })!;
|
||||||
|
expect(frame).toContain("above");
|
||||||
|
expect(frame).toContain("below");
|
||||||
|
const lines = frame.split("\n");
|
||||||
|
// "above", 2 blank lines, "below" = at least 4 lines
|
||||||
|
expect(lines.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
import { defineComponent, h } from "vue";
|
import { defineComponent, getCurrentInstance, h } from "vue";
|
||||||
|
|
||||||
export const Newline = defineComponent({
|
export const Newline = defineComponent({
|
||||||
name: "Newline",
|
name: "Newline",
|
||||||
props: { count: { type: Number, default: 1 } },
|
props: { count: { type: Number, default: 1 } },
|
||||||
setup(props) {
|
setup(props) {
|
||||||
return () => h("virtual-text", {}, "\n".repeat(props.count));
|
return () => {
|
||||||
|
const content = "\n".repeat(props.count);
|
||||||
|
// Inside a Text parent, render as inline virtual-text.
|
||||||
|
// Outside Text, render as "text" (yoga carrier) so Newline participates
|
||||||
|
// in layout standalone, matching Ink's ink-text behavior.
|
||||||
|
if (isInsideText()) {
|
||||||
|
return h("virtual-text", {}, content);
|
||||||
|
}
|
||||||
|
return h("text", {}, content);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isInsideText(): boolean {
|
||||||
|
let parent = getCurrentInstance()?.parent;
|
||||||
|
while (parent) {
|
||||||
|
const name = parent.type && (parent.type as { name?: string }).name;
|
||||||
|
if (name === "Text") return true;
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user