Skip to content

Commit 6d366d9

Browse files
feat(flow): add vertical orientation (#608)
* feat(flow): add vertical orientation Wires up the previously no-op `orientation="vertical"` prop on Flow. The connector path routing and parallel-branch geometry already handled the vertical axis; this fills the remaining gap in FlowNodeList's top-level connector computation and makes the node list lay out top-to-bottom, with `align` controlling cross-axis (horizontal) alignment. Adds vertical demos, docs, and tests. * chore: add changeset for flow vertical orientation
1 parent dd1a0b5 commit 6d366d9

6 files changed

Lines changed: 144 additions & 16 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/kumo": minor
3+
---
4+
5+
Add `orientation="vertical"` support to `Flow` for laying out nodes top-to-bottom. The `align` prop now controls cross-axis alignment in both orientations.

packages/kumo-docs-astro/src/components/demos/FlowDemo.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ export function FlowBasicDemo() {
4343
);
4444
}
4545

46+
/** Vertical flow diagram with sequential nodes */
47+
export function FlowVerticalDemo() {
48+
return (
49+
<Flow orientation="vertical">
50+
<Flow.Node>Step 1</Flow.Node>
51+
<Flow.Node>Step 2</Flow.Node>
52+
<Flow.Node>Step 3</Flow.Node>
53+
</Flow>
54+
);
55+
}
56+
57+
/** Vertical flow diagram with parallel branching */
58+
export function FlowVerticalParallelDemo() {
59+
return (
60+
<Flow orientation="vertical" align="center">
61+
<Flow.Node>Start</Flow.Node>
62+
<Flow.Parallel>
63+
<Flow.Node>Branch A</Flow.Node>
64+
<Flow.Node>Branch B</Flow.Node>
65+
<Flow.Node>Branch C</Flow.Node>
66+
</Flow.Parallel>
67+
<Flow.Node>End</Flow.Node>
68+
</Flow>
69+
);
70+
}
71+
4672
/** Flow diagram with parallel branching */
4773
export function FlowParallelDemo() {
4874
return (

packages/kumo-docs-astro/src/pages/components/flow.mdx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import ComponentExample from "~/components/docs/ComponentExample.astro";
99
import ComponentSection from "~/components/docs/ComponentSection.astro";
1010
import {
1111
FlowBasicDemo,
12+
FlowVerticalDemo,
13+
FlowVerticalParallelDemo,
1214
FlowParallelDemo,
1315
FlowCustomContentDemo,
1416
FlowComplexDemo,
@@ -100,6 +102,25 @@ export default function Example() {
100102
<FlowParallelDemo client:visible />
101103
</ComponentExample>
102104

105+
### Vertical Orientation
106+
107+
<p>
108+
Set `orientation="vertical"` to lay nodes out top-to-bottom instead of the
109+
default left-to-right. Connectors, parallel branches, and `align` all adapt
110+
to the vertical axis.
111+
</p>
112+
<ComponentExample demo="FlowVerticalDemo">
113+
<FlowVerticalDemo client:visible />
114+
</ComponentExample>
115+
116+
<p>
117+
Parallel branches work the same way in vertical flows. In this orientation
118+
`align` controls horizontal alignment of nodes.
119+
</p>
120+
<ComponentExample demo="FlowVerticalParallelDemo">
121+
<FlowVerticalParallelDemo client:visible />
122+
</ComponentExample>
123+
103124
### Custom Node Styling
104125

105126
<p>
@@ -238,12 +259,22 @@ export default function Example() {
238259
</tr>
239260
</thead>
240261
<tbody>
262+
<tr class="border-b border-kumo-hairline">
263+
<td class="px-4 py-3 font-mono">orientation</td>
264+
<td class="px-4 py-3 font-mono">"horizontal" | "vertical"</td>
265+
<td class="px-4 py-3">
266+
Layout direction of the flow. `"horizontal"` (default) lays nodes out
267+
left-to-right, `"vertical"` lays them out top-to-bottom.
268+
</td>
269+
</tr>
241270
<tr class="border-b border-kumo-hairline">
242271
<td class="px-4 py-3 font-mono">align</td>
243272
<td class="px-4 py-3 font-mono">"start" | "center"</td>
244273
<td class="px-4 py-3">
245-
Vertical alignment of nodes. `"start"` (default) aligns to top,
246-
`"center"` vertically centers nodes.
274+
Cross-axis alignment of nodes. In horizontal orientation this is
275+
vertical alignment; in vertical orientation it is horizontal
276+
alignment. `"start"` (default) aligns to the start of the cross axis,
277+
`"center"` centers nodes.
247278
</td>
248279
</tr>
249280
<tr class="border-b border-kumo-hairline">

packages/kumo-docs-astro/src/pages/tests/flow.astro

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import BaseLayout from "../../layouts/BaseLayout.astro";
33
import {
44
FlowBasicDemo,
5+
FlowVerticalDemo,
6+
FlowVerticalParallelDemo,
57
FlowParallelDemo,
68
FlowCustomContentDemo,
79
FlowComplexDemo,
@@ -28,6 +30,28 @@ import {
2830
</div>
2931
</section>
3032

33+
<section class="space-y-4">
34+
<h2 class="text-xl font-semibold text-kumo-default">FlowVerticalDemo</h2>
35+
<p class="text-kumo-subtle">
36+
Vertical flow diagram with sequential nodes
37+
</p>
38+
<div class="p-4 rounded-lg border border-kumo-hairline bg-kumo-base">
39+
<FlowVerticalDemo client:visible />
40+
</div>
41+
</section>
42+
43+
<section class="space-y-4">
44+
<h2 class="text-xl font-semibold text-kumo-default">
45+
FlowVerticalParallelDemo
46+
</h2>
47+
<p class="text-kumo-subtle">
48+
Vertical flow diagram with parallel branching
49+
</p>
50+
<div class="p-4 rounded-lg border border-kumo-hairline bg-kumo-base">
51+
<FlowVerticalParallelDemo client:visible />
52+
</div>
53+
</section>
54+
3155
<section class="space-y-4">
3256
<h2 class="text-xl font-semibold text-kumo-default">FlowParallelDemo</h2>
3357
<p class="text-kumo-subtle">Flow diagram with parallel branching</p>

packages/kumo/src/components/flow/diagram.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ function isEventFromNode(target: EventTarget | null): boolean {
4040
/** Minimum scrollbar thumb size in percentage to ensure visibility */
4141
const MIN_SCROLLBAR_THUMB_SIZE = 10;
4242

43-
// Vertical orientation is currently a no-op
4443
type Orientation = "horizontal" | "vertical";
4544
type Align = "start" | "center";
4645

@@ -66,9 +65,11 @@ export function useDiagramContext(): DiagramContextValue {
6665
interface FlowDiagramProps {
6766
orientation?: Orientation;
6867
/**
69-
* Controls vertical alignment of nodes in horizontal orientation.
70-
* - `start`: Nodes align to the top (default)
71-
* - `center`: Nodes are vertically centered
68+
* Controls cross-axis alignment of nodes.
69+
* In horizontal orientation this is vertical alignment; in vertical
70+
* orientation it is horizontal alignment.
71+
* - `start`: Nodes align to the start of the cross axis (default)
72+
* - `center`: Nodes are centered on the cross axis
7273
*/
7374
align?: Align;
7475
/**
@@ -428,11 +429,26 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
428429
if (currentRect && nextRect) {
429430
const isDisabled =
430431
currentNode.props.disabled || nextNode.props.disabled;
432+
// Horizontal flows left-to-right: connect the right edge of the
433+
// current node to the left edge of the next. Vertical flows
434+
// top-to-bottom: connect the bottom edge of the current node to the
435+
// top edge of the next, using horizontal centers.
436+
const edge =
437+
orientation === "vertical"
438+
? {
439+
x1: currentRect.left - offsetX + currentRect.width / 2,
440+
y1: currentRect.top - offsetY + currentRect.height,
441+
x2: nextRect.left - offsetX + nextRect.width / 2,
442+
y2: nextRect.top - offsetY,
443+
}
444+
: {
445+
x1: currentRect.left - offsetX + currentRect.width,
446+
y1: currentRect.top - offsetY + currentRect.height / 2,
447+
x2: nextRect.left - offsetX,
448+
y2: nextRect.top - offsetY + nextRect.height / 2,
449+
};
431450
edges.push({
432-
x1: currentRect.left - offsetX + currentRect.width,
433-
y1: currentRect.top - offsetY + currentRect.height / 2,
434-
x2: nextRect.left - offsetX,
435-
y2: nextRect.top - offsetY + nextRect.height / 2,
451+
...edge,
436452
disabled: isDisabled,
437453
single: true,
438454
fromId: currentNode.id,
@@ -442,7 +458,7 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
442458
}
443459

444460
setConnectors(edges);
445-
}, [descendants.descendants]);
461+
}, [descendants.descendants, orientation]);
446462

447463
/**
448464
* Recompute connectors after layout so that containerRect and node rects are
@@ -497,12 +513,13 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
497513
<div className="relative" ref={containerRef}>
498514
<ul
499515
className={cn(
500-
"ml-0 list-none",
516+
"ml-0 list-none gap-16",
501517
orientation === "vertical"
502-
? "grid auto-rows-min gap-16"
503-
: "flex gap-16",
504-
orientation === "horizontal" &&
505-
(align === "center" ? "items-center" : "items-start"),
518+
? cn(
519+
"flex flex-col w-fit",
520+
align === "center" ? "items-center" : "items-start",
521+
)
522+
: cn("flex", align === "center" ? "items-center" : "items-start"),
506523
)}
507524
>
508525
{children}

packages/kumo/src/components/flow/flow.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ describe("Flow", () => {
1919
expect(path).toBe("M 0 17 L 32 17 L 32 63 Q 32 71 40 71 L 48 71");
2020
expect(path).not.toContain(",");
2121
});
22+
23+
it("routes vertical paths through a horizontal mid-segment", () => {
24+
const path = createRoundedPath(
25+
{ x1: 17, y1: 0, x2: 71, y2: 56 },
26+
{ orientation: "vertical", single: false },
27+
);
28+
29+
expect(path).toContain("M 17 0");
30+
expect(path).not.toContain(",");
31+
});
32+
});
33+
34+
describe("Vertical orientation", () => {
35+
it("renders sequential nodes top-to-bottom in a column", () => {
36+
render(
37+
<Flow orientation="vertical">
38+
<Flow.Node>Step 1</Flow.Node>
39+
<Flow.Node>Step 2</Flow.Node>
40+
<Flow.Node>Step 3</Flow.Node>
41+
</Flow>,
42+
);
43+
44+
const list = screen.getByText("Step 1").closest("ul");
45+
expect(list?.className).toContain("flex-col");
46+
});
2247
});
2348

2449
describe("Compound component API", () => {

0 commit comments

Comments
 (0)