Svelte NodeToolbar with E2E (#3643)
* Added NodeToolbar * Added comments & fixed default behaviour in svelte * Added e2e-tests for NodeToolbar component & added data-id to react NodeToolbar * refactor(svelte/NodeToolbar): cleanup * refactor(node-toolbar): add system utils --------- Co-authored-by: moklick <info@moritzklack.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { memo, FC } from 'react';
|
||||
import { Handle, Position, NodeProps, NodeToolbar } from '@xyflow/react';
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id, data }) => {
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition} align={data.toolbarAlign}>
|
||||
<button>delete</button>
|
||||
<button>copy</button>
|
||||
<button>expand</button>
|
||||
</NodeToolbar>
|
||||
<div>{data.label}</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Position, type Node } from '@xyflow/react';
|
||||
import ToolbarNode from './components/ToolbarNode';
|
||||
|
||||
const positions = ['top', 'right', 'bottom', 'left'];
|
||||
const alignments = ['start', 'center', 'end'];
|
||||
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: 'default-node',
|
||||
type: 'ToolbarNode',
|
||||
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||
position: { x: 0, y: -200 },
|
||||
className: 'react-flow__node-default',
|
||||
},
|
||||
];
|
||||
|
||||
positions.forEach((position, posIndex) => {
|
||||
alignments.forEach((align, alignIndex) => {
|
||||
const id = `node-${align}-${position}`;
|
||||
nodes.push({
|
||||
id,
|
||||
type: 'ToolbarNode',
|
||||
data: {
|
||||
label: `toolbar ${position} ${align}`,
|
||||
toolbarPosition: position as Position,
|
||||
toolbarAlign: align,
|
||||
toolbarVisible: true,
|
||||
},
|
||||
className: 'react-flow__node-default',
|
||||
position: { x: posIndex * 300, y: alignIndex * 100 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default {
|
||||
flowProps: {
|
||||
fitView: true,
|
||||
nodeTypes: {
|
||||
ToolbarNode,
|
||||
},
|
||||
nodes,
|
||||
edges: [
|
||||
{
|
||||
id: 'first-edge',
|
||||
source: 'default-node',
|
||||
target: 'node-start-top',
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies FlowConfig;
|
||||
@@ -12,6 +12,7 @@
|
||||
'figma',
|
||||
'interaction',
|
||||
'intersections',
|
||||
'node-toolbar',
|
||||
'overview',
|
||||
'stress',
|
||||
'subflows',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<NodeToolbar
|
||||
isVisible={data.toolbarVisible}
|
||||
position={data.toolbarPosition}
|
||||
align={data.toolbarAlign}
|
||||
>
|
||||
<button>delete</button>
|
||||
<button>copy</button>
|
||||
<button>expand</button>
|
||||
</NodeToolbar>
|
||||
<div class="node">
|
||||
<div>{data.label}</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.node {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border: solid 1px black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Position, type Node } from '@xyflow/svelte';
|
||||
import ToolbarNode from './components/ToolbarNode.svelte';
|
||||
|
||||
const positions = ['top', 'right', 'bottom', 'left'];
|
||||
const alignments = ['start', 'center', 'end'];
|
||||
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: 'default-node',
|
||||
type: 'ToolbarNode',
|
||||
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||
position: { x: 0, y: -200 },
|
||||
class: 'react-flow__node-default'
|
||||
}
|
||||
];
|
||||
|
||||
positions.forEach((position, posIndex) => {
|
||||
alignments.forEach((align, alignIndex) => {
|
||||
const id = `node-${align}-${position}`;
|
||||
nodes.push({
|
||||
id,
|
||||
type: 'ToolbarNode',
|
||||
data: {
|
||||
label: `toolbar ${position} ${align}`,
|
||||
toolbarPosition: position as Position,
|
||||
toolbarAlign: align,
|
||||
toolbarVisible: true
|
||||
},
|
||||
class: 'react-flow__node-default',
|
||||
position: { x: posIndex * 300, y: alignIndex * 100 }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default {
|
||||
flowProps: {
|
||||
fitView: true,
|
||||
nodeTypes: {
|
||||
ToolbarNode
|
||||
},
|
||||
nodes,
|
||||
edges: [
|
||||
{
|
||||
id: 'first-edge',
|
||||
source: 'default-node',
|
||||
target: 'node-start-top'
|
||||
}
|
||||
]
|
||||
}
|
||||
} satisfies FlowConfig;
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import {
|
||||
SvelteFlow,
|
||||
Background,
|
||||
type Edge,
|
||||
type Node,
|
||||
Position,
|
||||
type NodeTypes
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
import CustomNode from './CustomNode.svelte';
|
||||
import SelectedNodesToolbar from './SelectedNodesToolbar.svelte';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode
|
||||
};
|
||||
|
||||
const positions = ['top', 'right', 'bottom', 'left'];
|
||||
const alignments = ['start', 'center', 'end'];
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: 'default-node',
|
||||
type: 'custom',
|
||||
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||
position: { x: 0, y: -200 },
|
||||
class: 'react-flow__node-default'
|
||||
}
|
||||
];
|
||||
|
||||
positions.forEach((position, posIndex) => {
|
||||
alignments.forEach((align, alignIndex) => {
|
||||
const id = `node-${align}-${position}`;
|
||||
initialNodes.push({
|
||||
id,
|
||||
type: 'custom',
|
||||
data: {
|
||||
label: `toolbar ${position} ${align}`,
|
||||
toolbarPosition: position as Position,
|
||||
toolbarAlign: align,
|
||||
toolbarVisible: true
|
||||
},
|
||||
class: 'react-flow__node-default',
|
||||
position: { x: posIndex * 300, y: alignIndex * 100 }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const nodes = writable<Node[]>(initialNodes);
|
||||
const edges = writable<Edge[]>(initialEdges);
|
||||
</script>
|
||||
|
||||
<div style="height: 100vh;">
|
||||
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
|
||||
<Background />
|
||||
<SelectedNodesToolbar />
|
||||
</SvelteFlow>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<NodeToolbar
|
||||
isVisible={data.toolbarVisible}
|
||||
position={data.toolbarPosition}
|
||||
align={data.toolbarAlign}
|
||||
>
|
||||
<button>delete</button>
|
||||
<button>copy</button>
|
||||
<button>expand</button>
|
||||
</NodeToolbar>
|
||||
<div class="node">
|
||||
<div>{data.label}</div>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.node {
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
border: solid 1px black;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { NodeToolbar, useNodes } from '@xyflow/svelte';
|
||||
|
||||
const nodes = useNodes();
|
||||
|
||||
$: selectedNodeIds = $nodes.filter((node) => node.selected).map((node) => node.id);
|
||||
$: isVisible = selectedNodeIds.length > 1;
|
||||
</script>
|
||||
|
||||
<NodeToolbar nodeId={selectedNodeIds} {isVisible}>
|
||||
<button>Selection action</button>
|
||||
</NodeToolbar>
|
||||
Reference in New Issue
Block a user