@likec4/diagram 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/KeepAspectRatioContainer.d.ts +9 -0
  2. package/dist/LikeC4Diagram.css.d.ts +1 -0
  3. package/dist/LikeC4Diagram.d.ts +1 -1
  4. package/dist/LikeC4Diagram.props.d.ts +16 -11
  5. package/dist/index.mjs +639 -762
  6. package/dist/state/DiagramContext.d.ts +54 -18
  7. package/dist/state/diagramStore.d.ts +61 -21
  8. package/dist/state/useDiagramStore.d.ts +54 -18
  9. package/dist/style.css +690 -79
  10. package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
  11. package/dist/ui/DiagramTitlePanel.d.ts +2 -0
  12. package/dist/ui/OptionsPanel.d.ts +1 -2
  13. package/dist/ui/index.d.ts +0 -1
  14. package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
  15. package/dist/xyflow/{XYFlowWrapper.d.ts → XYFlow.d.ts} +2 -2
  16. package/dist/xyflow/XYFlowEvents.d.ts +133 -13
  17. package/dist/xyflow/edges/RelationshipEdge.d.ts +2 -2
  18. package/dist/xyflow/edges/edges.css.d.ts +1 -0
  19. package/dist/xyflow/hooks/useXYFlow.d.ts +20 -20
  20. package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +7 -5
  21. package/dist/xyflow/types.d.ts +2 -2
  22. package/package.json +5 -3
  23. package/src/{ui/KeepAspectRatio.tsx → KeepAspectRatioContainer.tsx} +7 -6
  24. package/src/LikeC4Diagram.css.ts +8 -1
  25. package/src/LikeC4Diagram.props.ts +17 -12
  26. package/src/LikeC4Diagram.tsx +9 -6
  27. package/src/StaticLikeC4Diagram.tsx +1 -0
  28. package/src/globals.css.ts +48 -38
  29. package/src/state/diagramStore.ts +125 -68
  30. package/src/ui/DiagramTitlePanel.css.ts +32 -0
  31. package/src/ui/DiagramTitlePanel.tsx +139 -0
  32. package/src/ui/OptionsPanel.tsx +4 -23
  33. package/src/ui/index.ts +0 -1
  34. package/src/ui/options/NodeOptions.tsx +107 -30
  35. package/src/xyflow/FitviewOnDiagramChange.tsx +26 -20
  36. package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -0
  37. package/src/xyflow/SyncWithDiagram.tsx +35 -26
  38. package/src/xyflow/{XYFlowWrapper.tsx → XYFlow.tsx} +24 -27
  39. package/src/xyflow/XYFlowEvents.tsx +175 -81
  40. package/src/xyflow/diagram-to-xyflow.ts +4 -4
  41. package/src/xyflow/edges/RelationshipEdge.tsx +43 -47
  42. package/src/xyflow/edges/edges.css.ts +77 -36
  43. package/src/xyflow/nodes/compound/CompoundNode.css.ts +94 -44
  44. package/src/xyflow/nodes/compound/CompoundNode.tsx +29 -31
  45. package/src/xyflow/nodes/element/ElementNode.tsx +2 -2
  46. package/src/xyflow/nodes/element/element.css.ts +1 -0
  47. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +2 -1
  48. package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
  49. package/dist/ui/KeepAspectRatio.d.ts +0 -9
  50. package/src/ui/KeepAspectRatio.css.ts +0 -7
@@ -1,8 +1,9 @@
1
1
  import {
2
+ ElementShapes,
2
3
  defaultTheme,
4
+ invariant,
5
+ type BorderStyle,
3
6
  type ElementShape,
4
- ElementShapes,
5
- type Fqn,
6
7
  type NonEmptyArray,
7
8
  type ThemeColor
8
9
  } from '@likec4/core'
@@ -12,14 +13,18 @@ import {
12
13
  ColorSwatch,
13
14
  Divider,
14
15
  Flex,
15
- rem,
16
+ SegmentedControl,
16
17
  Select,
18
+ Slider,
17
19
  Stack,
18
20
  Text,
19
21
  Tooltip,
20
- TooltipGroup
22
+ TooltipGroup,
23
+ rem
21
24
  } from '@mantine/core'
25
+ import { useEffect, useState } from 'react'
22
26
  import { hasAtLeast, keys, takeWhile } from 'remeda'
27
+ import type { Changes } from '../../LikeC4Diagram.props'
23
28
  import { useDiagramStoreApi } from '../../state'
24
29
  import { useXYNodesData } from '../../xyflow/hooks'
25
30
  import { XYFlowNode } from '../../xyflow/types'
@@ -52,15 +57,34 @@ export type ColorKey = typeof colors[0]['key']
52
57
  type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
53
58
 
54
59
  export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] }) {
60
+ // export function NodeOptions(props: { nodes: XYFlowNode[] }) {
55
61
  const diagramApi = useDiagramStoreApi()
62
+ // const nodes = useXYNodesData(props.nodes.map(node => node.id))
56
63
  const nodes = useXYNodesData(selectedNodeIds)
57
64
  if (!hasAtLeast(nodes, 1)) {
58
65
  return null
59
66
  }
67
+ if (nodes.length !== selectedNodeIds.length) {
68
+ throw new Error('NodeOptions: nodes and props.nodes should have the same length')
69
+ }
60
70
  // Makes sense to show shape option only if there is at least one element node
61
71
  const showShapeOption = nodes.some(node => node.type === 'element')
62
72
 
63
73
  const [firstNode, ...rest] = nodes
74
+
75
+ // Makes sense to show opacity option only if there is at least one compound node
76
+ const showOpacityOption = rest.length === 0 && firstNode.type === 'compound'
77
+
78
+ const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
79
+ const targets = nodes.map(node => node.data.element.id)
80
+ invariant(hasAtLeast(targets, 1), 'At least one target is required')
81
+ diagramApi.getState().triggerOnChange([{
82
+ op: 'change-element-style',
83
+ style,
84
+ targets
85
+ }])
86
+ }
87
+
64
88
  return (
65
89
  <Stack>
66
90
  <Box maw={200}>
@@ -72,39 +96,33 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
72
96
  <ShapeOption
73
97
  nodes={nodes}
74
98
  onShapeChange={(shape: ElementShape) => {
75
- const targets = [] as Fqn[]
76
- for (const nd of nodes) {
77
- if (nd.data.element.shape !== shape) {
78
- targets.push(nd.data.element.id)
79
- }
80
- }
81
- if (hasAtLeast(targets, 1)) {
82
- diagramApi.getState().triggerOnChange([{
83
- op: 'change-shape',
84
- shape,
85
- targets
86
- }])
87
- }
99
+ triggerChange({ shape })
88
100
  }} />
89
101
  )}
90
102
  <Colors
91
103
  nodes={nodes}
92
104
  onColorChange={(color: ColorKey | ThemeColorKey) => {
93
- const targets = [] as Fqn[]
94
- for (const nd of nodes) {
95
- if (nd.data.element.color !== color) {
96
- targets.push(nd.data.element.id)
97
- }
98
- }
99
- if (hasAtLeast(targets, 1)) {
100
- diagramApi.getState().triggerOnChange([{
101
- op: 'change-color',
102
- color,
103
- targets
104
- }])
105
- }
105
+ triggerChange({ color })
106
106
  }} />
107
107
  <NavigateToOption nodes={nodes} />
108
+ <Box key={firstNode.id}>
109
+ {showOpacityOption && (
110
+ <>
111
+ <Divider label="opacity and border" labelPosition="left" />
112
+ <OpacityOption
113
+ node={firstNode}
114
+ onOpacityChange={(opacity: number) => {
115
+ triggerChange({ opacity })
116
+ }} />
117
+ <BorderStyleOption
118
+ node={firstNode}
119
+ onChange={(border: BorderStyle) => {
120
+ triggerChange({ border })
121
+ }}
122
+ />
123
+ </>
124
+ )}
125
+ </Box>
108
126
  </Stack>
109
127
  )
110
128
  }
@@ -251,3 +269,62 @@ function NavigateToOption({
251
269
  </Box>
252
270
  )
253
271
  }
272
+
273
+ function OpacityOption({
274
+ node,
275
+ onOpacityChange
276
+ }: {
277
+ node: XYNodesData
278
+ onOpacityChange: (opacity: number) => void
279
+ }) {
280
+ let selectedOpacity = node.data.element.style.opacity ?? 100
281
+ const [value, setValue] = useState(selectedOpacity)
282
+ useEffect(() => {
283
+ setValue(selectedOpacity)
284
+ }, [selectedOpacity])
285
+
286
+ return (
287
+ <Slider
288
+ mt={'xs'}
289
+ size={'sm'}
290
+ color={'dark'}
291
+ value={value}
292
+ onChange={setValue}
293
+ onChangeEnd={onOpacityChange} />
294
+ )
295
+ }
296
+
297
+ function BorderStyleOption({
298
+ node,
299
+ onChange
300
+ }: {
301
+ node: XYNodesData
302
+ onChange: (borderStyle: BorderStyle) => void
303
+ }) {
304
+ let selecteBorderStyle = node.data.element.style.border ?? 'dashed'
305
+ const [value, setValue] = useState(selecteBorderStyle)
306
+ useEffect(() => {
307
+ setValue(selecteBorderStyle)
308
+ }, [selecteBorderStyle])
309
+
310
+ return (
311
+ <Box mt={'md'}>
312
+ <SegmentedControl
313
+ size="xs"
314
+ fullWidth
315
+ withItemsBorders={false}
316
+ value={value}
317
+ onChange={v => {
318
+ setValue(v as BorderStyle)
319
+ onChange(v as BorderStyle)
320
+ }}
321
+ data={[
322
+ { label: 'Solid', value: 'solid' },
323
+ { label: 'Dashed', value: 'dashed' },
324
+ { label: 'Dotted', value: 'dotted' },
325
+ { label: 'None', value: 'none' }
326
+ ]}
327
+ />
328
+ </Box>
329
+ )
330
+ }
@@ -1,6 +1,5 @@
1
- import { useDebouncedEffect, useDebouncedState, useRafEffect } from '@react-hookz/web'
1
+ import { useDebouncedCallback, useDebouncedEffect, useRafEffect } from '@react-hookz/web'
2
2
  import { useNodesInitialized } from '@xyflow/react'
3
- import { shallowEqual } from 'fast-equals'
4
3
  import { memo, useEffect, useRef, useState } from 'react'
5
4
  import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
6
5
  import { useXYStore } from './hooks'
@@ -38,29 +37,36 @@ function FitViewOnDiagramChanges() {
38
37
  const diagramApi = useDiagramStoreApi()
39
38
  const [viewportMoved, setViewportMoved] = useState(diagramApi.getState().viewportChanged)
40
39
  const [processedChangeId, setProcessed] = useState(diagramChangeId(diagramApi.getState()))
41
- const [pendingChangeId, setPending] = useDebouncedState(processedChangeId, 100)
40
+ const [pendingChangeId, setPending] = useState(processedChangeId)
42
41
 
43
42
  const isReady = useNodesInitialized({
44
43
  includeHiddenNodes: true
45
44
  })
46
45
 
47
- useEffect(
48
- () => {
49
- return diagramApi.subscribe(
50
- s => ({
51
- viewportMoved: s.viewportChanged,
52
- changeid: diagramChangeId(s)
53
- }),
54
- ({ viewportMoved, changeid }) => {
55
- setPending(changeid)
56
- setViewportMoved(viewportMoved)
57
- },
58
- {
59
- equalityFn: shallowEqual
60
- }
61
- )
46
+ const schedulePending = useDebouncedCallback(
47
+ (pendingChangeId: string) => {
48
+ setPending(pendingChangeId)
62
49
  },
63
- [diagramApi]
50
+ [setPending],
51
+ 75
52
+ )
53
+
54
+ useEffect(
55
+ () =>
56
+ diagramApi.subscribe(
57
+ s => diagramChangeId(s),
58
+ schedulePending
59
+ ),
60
+ [diagramApi, schedulePending]
61
+ )
62
+
63
+ useEffect(
64
+ () =>
65
+ diagramApi.subscribe(
66
+ s => s.viewportChanged,
67
+ setViewportMoved
68
+ ),
69
+ [diagramApi, setViewportMoved]
64
70
  )
65
71
 
66
72
  useRafEffect(
@@ -71,7 +77,7 @@ function FitViewOnDiagramChanges() {
71
77
  diagramApi.getState().fitDiagram()
72
78
  setProcessed(pendingChangeId)
73
79
  },
74
- [isReady, processedChangeId, pendingChangeId]
80
+ [isReady, processedChangeId, pendingChangeId, diagramApi]
75
81
  )
76
82
 
77
83
  if (!viewportMoved && isReady) {
@@ -0,0 +1,57 @@
1
+ import type { EdgeChange, NodeChange } from '@xyflow/react'
2
+ import { useUpdateEffect } from '../hooks'
3
+ import { useDiagramState } from '../state/useDiagramStore'
4
+ import { useXYStoreApi } from './hooks/useXYFlow'
5
+ import type { XYFlowEdge, XYFlowNode } from './types'
6
+
7
+ export function SelectEdgesOnNodeFocus() {
8
+ const focusedNodeId = useDiagramState(s => s.focusedNodeId)
9
+ const xyflowApi = useXYStoreApi()
10
+
11
+ useUpdateEffect(() => {
12
+ const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
13
+ const nodeChanges = [] as NodeChange<XYFlowNode>[]
14
+ const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = xyflowApi.getState()
15
+ if (focusedNodeId) {
16
+ for (const edge of edges) {
17
+ if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
18
+ edgeChanges.push({
19
+ id: edge.id,
20
+ type: 'select',
21
+ selected: true
22
+ })
23
+ } else if (edge.selected) {
24
+ edgeChanges.push({
25
+ id: edge.id,
26
+ type: 'select',
27
+ selected: false
28
+ })
29
+ }
30
+ }
31
+ for (const node of nodes) {
32
+ if (node.selected && node.id !== focusedNodeId) {
33
+ nodeChanges.push({
34
+ id: node.id,
35
+ type: 'select',
36
+ selected: false
37
+ })
38
+ }
39
+ }
40
+ nodeChanges.push({
41
+ id: focusedNodeId,
42
+ type: 'select',
43
+ selected: true
44
+ })
45
+ if (edgeChanges.length > 0) {
46
+ triggerEdgeChanges(edgeChanges)
47
+ }
48
+ if (nodeChanges.length > 0) {
49
+ triggerNodeChanges(nodeChanges)
50
+ }
51
+ } else {
52
+ xyflowApi.getState().resetSelectedElements()
53
+ }
54
+ }, [focusedNodeId])
55
+
56
+ return null
57
+ }
@@ -27,14 +27,20 @@ export const SyncWithDiagram = memo(() => {
27
27
  state => ({
28
28
  viewId: state.view.id,
29
29
  nodes: state.view.nodes,
30
- edges: state.view.edges,
31
- draggable: state.nodesDraggable,
32
- selectable: state.nodesSelectable
30
+ edges: state.view.edges
33
31
  }),
34
32
  // listener
35
- ({ viewId, nodes, edges, ...opts }) => {
36
- const { lastOnNavigate } = diagramStoreApi.getState()
37
- const updates = diagramViewToXYFlowData({ nodes, edges }, opts)
33
+ ({ viewId, nodes, edges }) => {
34
+ const {
35
+ lastOnNavigate,
36
+ nodesDraggable,
37
+ nodesSelectable,
38
+ focusedNodeId
39
+ } = diagramStoreApi.getState()
40
+ const updates = diagramViewToXYFlowData({ nodes, edges }, {
41
+ draggable: nodesDraggable,
42
+ selectable: nodesSelectable || focusedNodeId !== null
43
+ })
38
44
 
39
45
  xyflowRef.current.setNodes(prev =>
40
46
  updates.nodes.map(update => {
@@ -65,33 +71,36 @@ export const SyncWithDiagram = memo(() => {
65
71
  })
66
72
  )
67
73
 
68
- if (lastOnNavigate?.toView === viewId && !lastOnNavigate.positionCorrected) {
74
+ if (lastOnNavigate?.toView === viewId && lastOnNavigate.positionCorrected !== true) {
69
75
  const elFrom = lastOnNavigate.element
70
76
  const elTo = nodes.find(n => n.id === elFrom.id)
71
77
  if (elTo) {
72
78
  const centerFrom = lastOnNavigate.elementCenterScreenPosition
73
- const nextZoom = Math.min(elFrom.width / elTo.width, elFrom.height / elTo.height)
79
+ let nextZoom = Math.min(elFrom.width / elTo.width, elFrom.height / elTo.height)
74
80
 
81
+ // TODO: investigate why 2 animation frames are needed
75
82
  requestAnimationFrame(() => {
76
- // requestAnimationFrame(() => {
77
- const v = xyflowRef.current.getViewport()
78
- if (nextZoom < v.zoom) {
79
- xyflowRef.current.setViewport({
80
- x: v.x,
81
- y: v.y,
82
- zoom: nextZoom
83
- })
84
- }
85
- const centerTo = xyflowRef.current.flowToScreenPosition({
86
- x: elTo.position[0] + elTo.width / 2,
87
- y: elTo.position[1] + elTo.height / 2
88
- }),
89
- diff = {
90
- x: toDomPrecision(centerFrom.x - centerTo.x),
91
- y: toDomPrecision(centerFrom.y - centerTo.y)
83
+ requestAnimationFrame(() => {
84
+ const v = xyflowRef.current.getViewport()
85
+ if (nextZoom < v.zoom) {
86
+ nextZoom = Math.max(nextZoom, v.zoom * 0.5)
87
+ xyflowRef.current.setViewport({
88
+ x: v.x,
89
+ y: v.y,
90
+ zoom: nextZoom
91
+ })
92
92
  }
93
- xyflowApi.getState().panBy(diff)
94
- // })
93
+
94
+ const centerTo = xyflowRef.current.flowToScreenPosition({
95
+ x: elTo.position[0] + elTo.width / 2,
96
+ y: elTo.position[1] + elTo.height / 2
97
+ }),
98
+ diff = {
99
+ x: toDomPrecision(centerFrom.x - centerTo.x),
100
+ y: toDomPrecision(centerFrom.y - centerTo.y)
101
+ }
102
+ xyflowApi.getState().panBy(diff)
103
+ })
95
104
  })
96
105
  }
97
106
  diagramStoreApi.setState(
@@ -1,5 +1,6 @@
1
1
  import { useMantineColorScheme } from '@mantine/core'
2
- import { Controls, ReactFlow } from '@xyflow/react'
2
+ import { Controls, PanOnScrollMode, ReactFlow } from '@xyflow/react'
3
+ import { DEV } from 'esm-env'
3
4
  import { type CSSProperties, memo, type PropsWithChildren } from 'react'
4
5
  import type { SetNonNullable, Simplify } from 'type-fest'
5
6
  import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
@@ -9,6 +10,7 @@ import { RelationshipEdge } from './edges/RelationshipEdge'
9
10
  import { useLayoutConstraints } from './hooks/useLayoutConstraints'
10
11
  import { CompoundNode } from './nodes/compound'
11
12
  import { ElementNode } from './nodes/element'
13
+ import { SelectEdgesOnNodeFocus } from './SelectEdgesOnNodeFocus'
12
14
  import { XYFlowEdge, XYFlowNode } from './types'
13
15
  import { XYFlowBackground } from './XYFlowBackground'
14
16
  import { useXYFlowEvents } from './XYFlowEvents'
@@ -26,8 +28,6 @@ type OnlyExpectedProps = Required<
26
28
  LikeC4DiagramProperties,
27
29
  | 'className'
28
30
  | 'controls'
29
- | 'pannable'
30
- | 'zoomable'
31
31
  | 'background'
32
32
  >
33
33
  >
@@ -43,7 +43,7 @@ type XYFlowWrapperProps = Simplify<
43
43
  >
44
44
 
45
45
  const selector = (s: DiagramState) => ({
46
- nodesSelectable: s.nodesSelectable,
46
+ nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
47
47
  nodesDraggable: s.nodesDraggable,
48
48
  fitView: s.fitViewEnabled,
49
49
  fitViewPadding: s.fitViewPadding,
@@ -53,7 +53,8 @@ const selector = (s: DiagramState) => ({
53
53
  hasOnCanvasContextMenu: !!s.onCanvasContextMenu,
54
54
  hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
55
55
  hasOnEdgeClick: !!s.onEdgeClick,
56
- hasOnCanvasClick: !!s.onCanvasClick || !!s.onCanvasDblClick
56
+ zoomable: s.zoomable,
57
+ pannable: s.pannable
57
58
  })
58
59
 
59
60
  function XYFlowWrapper({
@@ -61,8 +62,6 @@ function XYFlowWrapper({
61
62
  children,
62
63
  defaultNodes,
63
64
  defaultEdges,
64
- pannable,
65
- zoomable,
66
65
  controls,
67
66
  background,
68
67
  style
@@ -73,6 +72,8 @@ function XYFlowWrapper({
73
72
  nodesDraggable,
74
73
  fitView,
75
74
  fitViewPadding,
75
+ pannable,
76
+ zoomable,
76
77
  ...editor
77
78
  } = useDiagramState(selector)
78
79
 
@@ -120,33 +121,28 @@ function XYFlowWrapper({
120
121
  selectionKeyCode: null
121
122
  })}
122
123
  nodesDraggable={nodesDraggable}
123
- {...(nodesDraggable && layoutConstraints)}
124
+ {...nodesDraggable && {
125
+ onNodeDragStart: layoutConstraints.onNodeDragStart,
126
+ onNodeDrag: layoutConstraints.onNodeDrag,
127
+ onNodeDragStop: layoutConstraints.onNodeDragStop
128
+ }}
124
129
  zoomOnDoubleClick={false}
125
130
  elevateNodesOnSelect={false} // or edges are not visible after select
126
131
  selectNodesOnDrag={false} // or weird camera movement
132
+ onDoubleClick={handlers.onDoubleClick}
127
133
  onPaneClick={handlers.onPaneClick}
128
- {...(editor.hasOnNodeClick && {
129
- onNodeClick: handlers.onNodeClick
130
- })}
131
- {...(editor.hasOnEdgeClick && {
132
- onEdgeClick: handlers.onEdgeClick
133
- })}
134
134
  onMoveEnd={handlers.onMoveEnd}
135
+ onNodeMouseEnter={handlers.onNodeMouseEnter}
136
+ onNodeMouseLeave={handlers.onNodeMouseLeave}
137
+ onEdgeMouseEnter={handlers.onEdgeMouseEnter}
138
+ onEdgeMouseLeave={handlers.onEdgeMouseLeave}
139
+ onNodeClick={handlers.onNodeClick}
140
+ onNodeDoubleClick={handlers.onNodeDoubleClick}
141
+ onEdgeClick={handlers.onEdgeClick}
142
+ onViewportChange={handlers.onViewportChange}
135
143
  onInit={() => {
136
144
  diagramApi.setState({ initialized: true }, false, 'initialized')
137
145
  }}
138
- onNodeMouseEnter={(_event, node) => {
139
- diagramApi.getState().setHoveredNode(node.id)
140
- }}
141
- onNodeMouseLeave={() => {
142
- diagramApi.getState().setHoveredNode(null)
143
- }}
144
- onEdgeMouseEnter={(_event, edge) => {
145
- diagramApi.getState().setHoveredEdge(edge.id)
146
- }}
147
- onEdgeMouseLeave={() => {
148
- diagramApi.getState().setHoveredEdge(null)
149
- }}
150
146
  {...(editor.hasOnNodeContextMenu && {
151
147
  onNodeContextMenu: handlers.onNodeContextMenu
152
148
  })}
@@ -157,7 +153,8 @@ function XYFlowWrapper({
157
153
  onPaneContextMenu: handlers.onPaneContextMenu
158
154
  })}>
159
155
  {isBgWithPattern && <XYFlowBackground background={background} />}
160
- {controls && <Controls />}
156
+ {controls && <Controls position={'bottom-right'} />}
157
+ {zoomable && !editor.hasOnNodeClick && <SelectEdgesOnNodeFocus />}
161
158
  {children}
162
159
  </ReactFlow>
163
160
  )