@neo4j-nvl/react 1.0.0 → 1.1.0-01fc2de1
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.
- package/CHANGELOG.md +25 -0
- package/lib/__tests__/BasicNvlWrapper.test.js +70 -14
- package/lib/__tests__/InteractiveNvlWrapper.test.js +242 -3
- package/lib/__tests__/StaticPictureWrapper.test.d.ts +1 -0
- package/lib/__tests__/StaticPictureWrapper.test.js +85 -0
- package/lib/basic-wrapper/BasicNvlWrapper.d.ts +1 -1
- package/lib/basic-wrapper/BasicNvlWrapper.js +22 -9
- package/lib/index.d.ts +2 -2
- package/lib/interactive-nvl-wrapper/InteractionHandlers.d.ts +2 -1
- package/lib/interactive-nvl-wrapper/InteractionHandlers.js +15 -2
- package/lib/interactive-nvl-wrapper/InteractiveNvlWrapper.d.ts +1 -2
- package/lib/interactive-nvl-wrapper/InteractiveNvlWrapper.js +2 -2
- package/lib/interactive-nvl-wrapper/hooks.d.ts +3 -3
- package/lib/interactive-nvl-wrapper/types.d.ts +12 -4
- package/lib/static-picture-wrapper/StaticPictureWrapper.d.ts +21 -4
- package/lib/static-picture-wrapper/StaticPictureWrapper.js +32 -7
- package/lib/utils/constants.d.ts +2 -2
- package/package.json +15 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NVL will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
4
|
|
|
5
|
+
## [1.1.0] - 2026-02-16
|
|
6
|
+
|
|
7
|
+
This 1.1.0 release introduces new rendering capabilities with the addition of a new SVG export method, alongside a new Circular Layout algorithm and touchpad gestures for zooming. It also includes performance improvements for the renderer and physics engine, as well as various fixes for the React wrappers and Minimap.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- new SVG export functionality.
|
|
12
|
+
- new circular layout option.
|
|
13
|
+
- two-finger pinch gesture to zoom with touchpads.
|
|
14
|
+
- onCanvasDoubleClick callback to the interactive NVL React wrapper.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- improved minimap handling for React components.
|
|
19
|
+
- rendering performance improvements.
|
|
20
|
+
- physics layout performance and gravity improvements.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- seed radius calculation in D3 layout for large graphs.
|
|
25
|
+
- forceDirected layout unnecessarily reheating on switch without changes.
|
|
26
|
+
- ensure drag operations are always correctly ended on mouse up.
|
|
27
|
+
- minimap positioning issues.
|
|
28
|
+
- background color for the 'os' theme in api documentation.
|
|
29
|
+
|
|
5
30
|
## [1.0.0] - 2025-09-30
|
|
6
31
|
This 1.0.0 release is our first major release, with NVL's release strategy fully adopting semantic versioning going forward. Updates in this release include React 19 support for the React wrappers, a UI overhaul of the [examples app](https://neo4j.com/docs/api/nvl/current/examples.html).
|
|
7
32
|
|
|
@@ -2,11 +2,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import NVL, { HierarchicalLayoutType } from '@neo4j-nvl/base';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
4
|
import { render } from '@testing-library/react';
|
|
5
|
-
import
|
|
5
|
+
import { StrictMode } from 'react';
|
|
6
6
|
import { BasicNvlWrapper } from '../basic-wrapper/BasicNvlWrapper';
|
|
7
7
|
jest.mock('@neo4j-nvl/base');
|
|
8
8
|
jest.mock('@neo4j-nvl/layout-workers');
|
|
9
9
|
describe('BasicNvlWrapper', () => {
|
|
10
|
+
let setZoom;
|
|
11
|
+
let setPan;
|
|
12
|
+
let setZoomAndPan;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
setZoom = jest.fn();
|
|
15
|
+
setPan = jest.fn();
|
|
16
|
+
setZoomAndPan = jest.fn();
|
|
17
|
+
jest.spyOn(NVL.prototype, 'setZoom').mockImplementation(setZoom);
|
|
18
|
+
jest.spyOn(NVL.prototype, 'setPan').mockImplementation(setPan);
|
|
19
|
+
jest.spyOn(NVL.prototype, 'setZoomAndPan').mockImplementation(setZoomAndPan);
|
|
20
|
+
});
|
|
10
21
|
afterEach(() => {
|
|
11
22
|
jest.clearAllMocks();
|
|
12
23
|
});
|
|
@@ -32,38 +43,83 @@ describe('BasicNvlWrapper', () => {
|
|
|
32
43
|
test('successfully re-initialises NVL when using React StrictMode', () => {
|
|
33
44
|
const destroy = jest.fn();
|
|
34
45
|
jest.spyOn(NVL.prototype, 'destroy').mockImplementation(destroy);
|
|
35
|
-
render(_jsx(
|
|
46
|
+
render(_jsx(StrictMode, { children: _jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [] }) }) }));
|
|
36
47
|
expect(NVL).toHaveBeenCalledTimes(2);
|
|
37
48
|
expect(destroy).toHaveBeenCalledTimes(1);
|
|
38
49
|
});
|
|
39
50
|
test('calls setZoom when zoom property is provided', () => {
|
|
40
|
-
const setZoom = jest.fn();
|
|
41
|
-
jest.spyOn(NVL.prototype, 'setZoom').mockImplementation(setZoom);
|
|
42
51
|
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5 }) }));
|
|
43
52
|
expect(setZoom).toHaveBeenCalledWith(1.5);
|
|
53
|
+
expect(setZoom).toHaveBeenCalledTimes(1);
|
|
44
54
|
});
|
|
45
55
|
test('calls setPan when pan property is provided', () => {
|
|
46
|
-
const setPan = jest.fn();
|
|
47
|
-
jest.spyOn(NVL.prototype, 'setPan').mockImplementation(setPan);
|
|
48
56
|
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], pan: { x: 10, y: 20 } }) }));
|
|
49
57
|
expect(setPan).toHaveBeenCalledWith(10, 20);
|
|
58
|
+
expect(setPan).toHaveBeenCalledTimes(1);
|
|
50
59
|
});
|
|
51
60
|
test('calls setZoomAndPan when both zoom and pan properties are provided', () => {
|
|
52
|
-
const setZoomAndPan = jest.fn();
|
|
53
|
-
jest.spyOn(NVL.prototype, 'setZoomAndPan').mockImplementation(setZoomAndPan);
|
|
54
61
|
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
55
62
|
expect(setZoomAndPan).toHaveBeenCalledWith(1.5, 10, 20);
|
|
63
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
56
64
|
});
|
|
57
65
|
test('does not call zoom/pan methods when properties are undefined', () => {
|
|
58
|
-
const setZoom = jest.fn();
|
|
59
|
-
jest.spyOn(NVL.prototype, 'setZoom').mockImplementation(setZoom);
|
|
60
|
-
const setPan = jest.fn();
|
|
61
|
-
jest.spyOn(NVL.prototype, 'setPan').mockImplementation(setPan);
|
|
62
|
-
const setZoomAndPan = jest.fn();
|
|
63
|
-
jest.spyOn(NVL.prototype, 'setZoomAndPan').mockImplementation(setZoomAndPan);
|
|
64
66
|
render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [] }) }));
|
|
65
67
|
expect(setZoom).not.toHaveBeenCalled();
|
|
66
68
|
expect(setPan).not.toHaveBeenCalled();
|
|
67
69
|
expect(setZoomAndPan).not.toHaveBeenCalled();
|
|
68
70
|
});
|
|
71
|
+
test('only calls setZoom when zoom changes', () => {
|
|
72
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5 }) }));
|
|
73
|
+
expect(setZoom).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(setZoom).toHaveBeenCalledWith(1.5);
|
|
75
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5 }) }));
|
|
76
|
+
expect(setZoom).toHaveBeenCalledTimes(1);
|
|
77
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 2.0 }) }));
|
|
78
|
+
expect(setZoom).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(setZoom).toHaveBeenCalledWith(2.0);
|
|
80
|
+
expect(setPan).not.toHaveBeenCalled();
|
|
81
|
+
expect(setZoomAndPan).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
test('only calls setPan when pan changes', () => {
|
|
84
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], pan: { x: 10, y: 20 } }) }));
|
|
85
|
+
expect(setPan).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(setPan).toHaveBeenCalledWith(10, 20);
|
|
87
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], pan: { x: 10, y: 20 } }) }));
|
|
88
|
+
expect(setPan).toHaveBeenCalledTimes(1);
|
|
89
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], pan: { x: 30, y: 40 } }) }));
|
|
90
|
+
expect(setPan).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(setPan).toHaveBeenCalledWith(30, 40);
|
|
92
|
+
expect(setZoom).not.toHaveBeenCalled();
|
|
93
|
+
expect(setZoomAndPan).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
test('calls setZoomAndPan when both zoom and pan change', () => {
|
|
96
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
97
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(setZoomAndPan).toHaveBeenCalledWith(1.5, 10, 20);
|
|
99
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
100
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
101
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 2.0, pan: { x: 30, y: 40 } }) }));
|
|
102
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(2);
|
|
103
|
+
expect(setZoomAndPan).toHaveBeenCalledWith(2.0, 30, 40);
|
|
104
|
+
expect(setZoom).not.toHaveBeenCalled();
|
|
105
|
+
expect(setPan).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
test('calls appropriate methods when only zoom changes while both are set', () => {
|
|
108
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
109
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
110
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 2.0, pan: { x: 10, y: 20 } }) }));
|
|
111
|
+
expect(setZoom).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(setZoom).toHaveBeenCalledWith(2.0);
|
|
113
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(setPan).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
test('calls appropriate methods when only pan changes while both are set', () => {
|
|
117
|
+
const { rerender } = render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }));
|
|
118
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
119
|
+
rerender(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 30, y: 40 } }) }));
|
|
120
|
+
expect(setPan).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(setPan).toHaveBeenCalledWith(30, 40);
|
|
122
|
+
expect(setZoomAndPan).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(setZoom).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
69
125
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import NVL, { HierarchicalLayoutType } from '@neo4j-nvl/base';
|
|
3
|
-
import { BoxSelectInteraction, ClickInteraction, DragNodeInteraction, DrawInteraction, HoverInteraction, LassoInteraction, PanInteraction, ZoomInteraction } from '@neo4j-nvl/interaction-handlers';
|
|
3
|
+
import { BoxSelectInteraction, ClickInteraction, DragNodeInteraction, DrawInteraction, HoverInteraction, KeyboardInteraction, LassoInteraction, PanInteraction, ZoomInteraction } from '@neo4j-nvl/interaction-handlers';
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
5
|
import { render } from '@testing-library/react';
|
|
6
|
-
import
|
|
6
|
+
import { StrictMode, act, createRef } from 'react';
|
|
7
7
|
import { InteractiveNvlWrapper } from '../interactive-nvl-wrapper/InteractiveNvlWrapper';
|
|
8
8
|
jest.mock('@neo4j-nvl/base');
|
|
9
9
|
jest.mock('@neo4j-nvl/layout-workers');
|
|
@@ -11,7 +11,41 @@ jest.mock('@neo4j-nvl/interaction-handlers');
|
|
|
11
11
|
const MockedNVL = NVL;
|
|
12
12
|
let mockOnInitialization;
|
|
13
13
|
HoverInteraction.prototype.callbackMap = new Map();
|
|
14
|
+
HoverInteraction.prototype.updateCallback = jest.fn();
|
|
15
|
+
HoverInteraction.prototype.removeCallback = jest.fn();
|
|
16
|
+
HoverInteraction.prototype.destroy = jest.fn();
|
|
14
17
|
PanInteraction.prototype.callbackMap = new Map();
|
|
18
|
+
PanInteraction.prototype.updateCallback = jest.fn();
|
|
19
|
+
PanInteraction.prototype.removeCallback = jest.fn();
|
|
20
|
+
PanInteraction.prototype.destroy = jest.fn();
|
|
21
|
+
ClickInteraction.prototype.callbackMap = new Map();
|
|
22
|
+
ClickInteraction.prototype.updateCallback = jest.fn();
|
|
23
|
+
ClickInteraction.prototype.removeCallback = jest.fn();
|
|
24
|
+
ClickInteraction.prototype.destroy = jest.fn();
|
|
25
|
+
ZoomInteraction.prototype.callbackMap = new Map();
|
|
26
|
+
ZoomInteraction.prototype.updateCallback = jest.fn();
|
|
27
|
+
ZoomInteraction.prototype.removeCallback = jest.fn();
|
|
28
|
+
ZoomInteraction.prototype.destroy = jest.fn();
|
|
29
|
+
DragNodeInteraction.prototype.callbackMap = new Map();
|
|
30
|
+
DragNodeInteraction.prototype.updateCallback = jest.fn();
|
|
31
|
+
DragNodeInteraction.prototype.removeCallback = jest.fn();
|
|
32
|
+
DragNodeInteraction.prototype.destroy = jest.fn();
|
|
33
|
+
DrawInteraction.prototype.callbackMap = new Map();
|
|
34
|
+
DrawInteraction.prototype.updateCallback = jest.fn();
|
|
35
|
+
DrawInteraction.prototype.removeCallback = jest.fn();
|
|
36
|
+
DrawInteraction.prototype.destroy = jest.fn();
|
|
37
|
+
BoxSelectInteraction.prototype.callbackMap = new Map();
|
|
38
|
+
BoxSelectInteraction.prototype.updateCallback = jest.fn();
|
|
39
|
+
BoxSelectInteraction.prototype.removeCallback = jest.fn();
|
|
40
|
+
BoxSelectInteraction.prototype.destroy = jest.fn();
|
|
41
|
+
LassoInteraction.prototype.callbackMap = new Map();
|
|
42
|
+
LassoInteraction.prototype.updateCallback = jest.fn();
|
|
43
|
+
LassoInteraction.prototype.removeCallback = jest.fn();
|
|
44
|
+
LassoInteraction.prototype.destroy = jest.fn();
|
|
45
|
+
KeyboardInteraction.prototype.callbackMap = new Map();
|
|
46
|
+
KeyboardInteraction.prototype.updateCallback = jest.fn();
|
|
47
|
+
KeyboardInteraction.prototype.removeCallback = jest.fn();
|
|
48
|
+
KeyboardInteraction.prototype.destroy = jest.fn();
|
|
15
49
|
let mockDestroy;
|
|
16
50
|
let destroyHoverInteraction;
|
|
17
51
|
let destroyPanInteraction;
|
|
@@ -34,6 +68,13 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
34
68
|
jest.spyOn(NVL.prototype, 'destroy').mockImplementation(mockDestroy);
|
|
35
69
|
jest.spyOn(HoverInteraction.prototype, 'destroy').mockImplementation(destroyHoverInteraction);
|
|
36
70
|
jest.spyOn(PanInteraction.prototype, 'destroy').mockImplementation(destroyPanInteraction);
|
|
71
|
+
jest.spyOn(ClickInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
72
|
+
jest.spyOn(ZoomInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
73
|
+
jest.spyOn(DragNodeInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
74
|
+
jest.spyOn(DrawInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
75
|
+
jest.spyOn(BoxSelectInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
76
|
+
jest.spyOn(LassoInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
77
|
+
jest.spyOn(KeyboardInteraction.prototype, 'destroy').mockImplementation(jest.fn());
|
|
37
78
|
});
|
|
38
79
|
afterEach(() => {
|
|
39
80
|
jest.clearAllMocks();
|
|
@@ -74,6 +115,191 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
74
115
|
expect(ZoomInteraction).not.toHaveBeenCalled();
|
|
75
116
|
expect(BoxSelectInteraction).not.toHaveBeenCalled();
|
|
76
117
|
expect(LassoInteraction).not.toHaveBeenCalled();
|
|
118
|
+
expect(KeyboardInteraction).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
test.each([
|
|
121
|
+
{
|
|
122
|
+
name: 'HoverInteraction',
|
|
123
|
+
InteractionClass: HoverInteraction,
|
|
124
|
+
callback: { onHover: jest.fn() },
|
|
125
|
+
options: { drawShadowOnHover: true }
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'ClickInteraction with onCanvasClick',
|
|
129
|
+
InteractionClass: ClickInteraction,
|
|
130
|
+
callback: { onCanvasClick: jest.fn() },
|
|
131
|
+
options: { selectOnClick: false }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'ClickInteraction with onCanvasDoubleClick',
|
|
135
|
+
InteractionClass: ClickInteraction,
|
|
136
|
+
callback: { onCanvasDoubleClick: jest.fn() },
|
|
137
|
+
options: { selectOnClick: false }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'ClickInteraction with onCanvasRightClick',
|
|
141
|
+
InteractionClass: ClickInteraction,
|
|
142
|
+
callback: { onCanvasRightClick: jest.fn() },
|
|
143
|
+
options: { selectOnClick: false }
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'ClickInteraction with onNodeClick',
|
|
147
|
+
InteractionClass: ClickInteraction,
|
|
148
|
+
callback: { onNodeClick: jest.fn() },
|
|
149
|
+
options: { selectOnClick: true }
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'ClickInteraction with onNodeDoubleClick',
|
|
153
|
+
InteractionClass: ClickInteraction,
|
|
154
|
+
callback: { onNodeDoubleClick: jest.fn() },
|
|
155
|
+
options: { selectOnClick: false }
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'ClickInteraction with onNodeRightClick',
|
|
159
|
+
InteractionClass: ClickInteraction,
|
|
160
|
+
callback: { onNodeRightClick: jest.fn() },
|
|
161
|
+
options: { selectOnClick: false }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'ClickInteraction with onRelationshipClick',
|
|
165
|
+
InteractionClass: ClickInteraction,
|
|
166
|
+
callback: { onRelationshipClick: jest.fn() },
|
|
167
|
+
options: { selectOnClick: false }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'ClickInteraction with onRelationshipDoubleClick',
|
|
171
|
+
InteractionClass: ClickInteraction,
|
|
172
|
+
callback: { onRelationshipDoubleClick: jest.fn() },
|
|
173
|
+
options: { selectOnClick: false }
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'ClickInteraction with onRelationshipRightClick',
|
|
177
|
+
InteractionClass: ClickInteraction,
|
|
178
|
+
callback: { onRelationshipRightClick: jest.fn() },
|
|
179
|
+
options: { selectOnClick: false }
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'PanInteraction',
|
|
183
|
+
InteractionClass: PanInteraction,
|
|
184
|
+
callback: { onPan: jest.fn() },
|
|
185
|
+
options: {}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'ZoomInteraction',
|
|
189
|
+
InteractionClass: ZoomInteraction,
|
|
190
|
+
callback: { onZoom: jest.fn() },
|
|
191
|
+
options: {}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'DragNodeInteraction with onDrag',
|
|
195
|
+
InteractionClass: DragNodeInteraction,
|
|
196
|
+
callback: { onDrag: jest.fn() },
|
|
197
|
+
options: { selectOnRelease: false }
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'DragNodeInteraction with onDragStart',
|
|
201
|
+
InteractionClass: DragNodeInteraction,
|
|
202
|
+
callback: { onDragStart: jest.fn() },
|
|
203
|
+
options: {}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'DragNodeInteraction with onDragEnd',
|
|
207
|
+
InteractionClass: DragNodeInteraction,
|
|
208
|
+
callback: { onDragEnd: jest.fn() },
|
|
209
|
+
options: {}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'DrawInteraction with onHoverNodeMargin',
|
|
213
|
+
InteractionClass: DrawInteraction,
|
|
214
|
+
callback: { onHoverNodeMargin: jest.fn() },
|
|
215
|
+
options: { excludeNodeMargin: false }
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'DrawInteraction with onDrawStarted',
|
|
219
|
+
InteractionClass: DrawInteraction,
|
|
220
|
+
callback: { onDrawStarted: jest.fn() },
|
|
221
|
+
options: {}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'DrawInteraction with onDrawEnded',
|
|
225
|
+
InteractionClass: DrawInteraction,
|
|
226
|
+
callback: { onDrawEnded: jest.fn() },
|
|
227
|
+
options: {}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'BoxSelectInteraction with onBoxStarted',
|
|
231
|
+
InteractionClass: BoxSelectInteraction,
|
|
232
|
+
callback: { onBoxStarted: jest.fn() },
|
|
233
|
+
options: {}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'BoxSelectInteraction with onBoxSelect',
|
|
237
|
+
InteractionClass: BoxSelectInteraction,
|
|
238
|
+
callback: { onBoxSelect: jest.fn() },
|
|
239
|
+
options: {}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'LassoInteraction with onLassoStarted',
|
|
243
|
+
InteractionClass: LassoInteraction,
|
|
244
|
+
callback: { onLassoStarted: jest.fn() },
|
|
245
|
+
options: {}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'LassoInteraction with onLassoSelect',
|
|
249
|
+
InteractionClass: LassoInteraction,
|
|
250
|
+
callback: { onLassoSelect: jest.fn() },
|
|
251
|
+
options: {}
|
|
252
|
+
}
|
|
253
|
+
])('initialises $name when its callback is provided', ({ InteractionClass, callback, options }) => {
|
|
254
|
+
const myNvlRef = createRef();
|
|
255
|
+
render(_jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, mouseEventCallbacks: callback, interactionOptions: options }));
|
|
256
|
+
act(() => {
|
|
257
|
+
mockOnInitialization?.();
|
|
258
|
+
});
|
|
259
|
+
expect(InteractionClass).toHaveBeenCalledWith(myNvlRef.current, options);
|
|
260
|
+
expect(InteractionClass).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
test.each([
|
|
263
|
+
{
|
|
264
|
+
name: 'KeyboardInteraction with onKeyDown',
|
|
265
|
+
callback: { onKeyDown: jest.fn() }
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'KeyboardInteraction with onKeyUp',
|
|
269
|
+
callback: { onKeyUp: jest.fn() }
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'KeyboardInteraction with onNodeFocus',
|
|
273
|
+
callback: { onNodeFocus: jest.fn() }
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'KeyboardInteraction with onNodeBlur',
|
|
277
|
+
callback: { onNodeBlur: jest.fn() }
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'KeyboardInteraction with onRelationshipFocus',
|
|
281
|
+
callback: { onRelationshipFocus: jest.fn() }
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'KeyboardInteraction with onRelationshipBlur',
|
|
285
|
+
callback: { onRelationshipBlur: jest.fn() }
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: 'KeyboardInteraction with onCanvasFocus',
|
|
289
|
+
callback: { onCanvasFocus: jest.fn() }
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'KeyboardInteraction with onCanvasBlur',
|
|
293
|
+
callback: { onCanvasBlur: jest.fn() }
|
|
294
|
+
}
|
|
295
|
+
])('initialises $name when its callback is provided', ({ callback }) => {
|
|
296
|
+
const myNvlRef = createRef();
|
|
297
|
+
render(_jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, keyboardEventCallbacks: callback }));
|
|
298
|
+
act(() => {
|
|
299
|
+
mockOnInitialization?.();
|
|
300
|
+
});
|
|
301
|
+
expect(KeyboardInteraction).toHaveBeenCalledWith(myNvlRef.current, expect.objectContaining({}));
|
|
302
|
+
expect(KeyboardInteraction).toHaveBeenCalledTimes(1);
|
|
77
303
|
});
|
|
78
304
|
test('does not attempt to initialize interaction handlers if NVL container is not available', () => {
|
|
79
305
|
const myNvlRef = createRef();
|
|
@@ -102,7 +328,7 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
102
328
|
});
|
|
103
329
|
test('successfully re-initialises NVL and active interaction handlers when using React StrictMode', () => {
|
|
104
330
|
const myNvlRef = createRef();
|
|
105
|
-
render(_jsx(
|
|
331
|
+
render(_jsx(StrictMode, { children: _jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, mouseEventCallbacks: {
|
|
106
332
|
onHover: true
|
|
107
333
|
}, interactionOptions: {
|
|
108
334
|
drawShadowOnHover: true
|
|
@@ -121,6 +347,19 @@ describe('InteractiveNvlWrapper', () => {
|
|
|
121
347
|
expect(ZoomInteraction).not.toHaveBeenCalled();
|
|
122
348
|
expect(BoxSelectInteraction).not.toHaveBeenCalled();
|
|
123
349
|
expect(LassoInteraction).not.toHaveBeenCalled();
|
|
350
|
+
expect(KeyboardInteraction).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
test('destroys keyboard interaction on unmount', () => {
|
|
353
|
+
const destroyKeyboardInteraction = jest.fn();
|
|
354
|
+
jest.spyOn(KeyboardInteraction.prototype, 'destroy').mockImplementation(destroyKeyboardInteraction);
|
|
355
|
+
const { unmount } = render(_jsx(InteractiveNvlWrapper, { nodes: [], rels: [], keyboardEventCallbacks: {
|
|
356
|
+
onKeyDown: true
|
|
357
|
+
} }));
|
|
358
|
+
act(() => {
|
|
359
|
+
mockOnInitialization?.();
|
|
360
|
+
});
|
|
361
|
+
unmount();
|
|
362
|
+
expect(destroyKeyboardInteraction).toHaveBeenCalledTimes(1);
|
|
124
363
|
});
|
|
125
364
|
test('also calls custom callbacks on initialization', () => {
|
|
126
365
|
const customOnInitializationCallback = jest.fn();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { render, waitFor } from '@testing-library/react';
|
|
4
|
+
import { StaticPictureWrapper } from '../static-picture-wrapper/StaticPictureWrapper';
|
|
5
|
+
jest.mock('@neo4j-nvl/base', () => {
|
|
6
|
+
const mockNVL = jest
|
|
7
|
+
.fn()
|
|
8
|
+
.mockImplementation((_element, _nodes, _rels, _nvlOptions, callbacks) => {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
callbacks.onLayoutDone?.();
|
|
11
|
+
callbacks.onZoomTransitionDone?.();
|
|
12
|
+
}, 0);
|
|
13
|
+
return {
|
|
14
|
+
getNodes: jest.fn().mockReturnValue([
|
|
15
|
+
{ id: '0', x: 0, y: 0 },
|
|
16
|
+
{ id: '1', x: 100, y: 100 }
|
|
17
|
+
]),
|
|
18
|
+
fit: jest.fn(),
|
|
19
|
+
getImageDataUrl: jest.fn().mockReturnValue('data:image/png;base64,...mock-png...'),
|
|
20
|
+
getSvgDataUrl: jest.fn().mockResolvedValue('data:image/svg+xml;charset=utf-8,...mock-svg...'),
|
|
21
|
+
destroy: jest.fn()
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return mockNVL;
|
|
25
|
+
});
|
|
26
|
+
const NVLMock = jest.requireMock('@neo4j-nvl/base');
|
|
27
|
+
describe('StaticPictureWrapper - format prop support', () => {
|
|
28
|
+
const mockNodes = [
|
|
29
|
+
{ id: '0', caption: 'Node 0' },
|
|
30
|
+
{ id: '1', caption: 'Node 1' }
|
|
31
|
+
];
|
|
32
|
+
const mockRels = [{ id: '01', from: '0', to: '1', caption: 'KNOWS' }];
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
NVLMock.mockClear();
|
|
35
|
+
});
|
|
36
|
+
const getMockNVLInstance = (index = 0) => {
|
|
37
|
+
return NVLMock.mock.results[index]?.value;
|
|
38
|
+
};
|
|
39
|
+
it('Should pass nodes, relationships and options to NVL constructor', () => {
|
|
40
|
+
const mockOptions = {
|
|
41
|
+
layout: 'hierarchical',
|
|
42
|
+
styling: {
|
|
43
|
+
defaultNodeColor: 'red'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
render(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "png", nvlOptions: mockOptions, width: 250, height: 250 }));
|
|
47
|
+
expect(NVLMock).toHaveBeenCalledWith(expect.any(HTMLDivElement), mockNodes, mockRels, mockOptions, expect.any(Object));
|
|
48
|
+
});
|
|
49
|
+
it('Should use getSvgDataUrl for SVG format', async () => {
|
|
50
|
+
render(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "svg" }));
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
const mockInstance = getMockNVLInstance();
|
|
53
|
+
expect(mockInstance.getSvgDataUrl).toHaveBeenCalled();
|
|
54
|
+
expect(mockInstance.getImageDataUrl).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('Should use getImageDataUrl for PNG format', async () => {
|
|
58
|
+
render(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "png" }));
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
const mockInstance = getMockNVLInstance();
|
|
61
|
+
expect(mockInstance.getSvgDataUrl).not.toHaveBeenCalled();
|
|
62
|
+
expect(mockInstance.getImageDataUrl).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('Should triggerer re-render when format prop changes', async () => {
|
|
66
|
+
const { rerender } = render(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "png" }));
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
const mockInstance = getMockNVLInstance(0);
|
|
69
|
+
expect(mockInstance.getSvgDataUrl).not.toHaveBeenCalled();
|
|
70
|
+
expect(mockInstance.getImageDataUrl).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
rerender(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "svg" }));
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
const mockInstance = getMockNVLInstance(1);
|
|
75
|
+
expect(mockInstance.getSvgDataUrl).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(mockInstance.getImageDataUrl).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('Should clean up NVL instance on unmount', () => {
|
|
80
|
+
const { unmount } = render(_jsx(StaticPictureWrapper, { nodes: mockNodes, rels: mockRels, format: "png" }));
|
|
81
|
+
unmount();
|
|
82
|
+
const mockInstance = getMockNVLInstance();
|
|
83
|
+
expect(mockInstance.destroy).toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -45,4 +45,4 @@ export interface BasicReactWrapperProps {
|
|
|
45
45
|
*
|
|
46
46
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_basic_react_wrapper Basic React wrapper documentation page}.
|
|
47
47
|
*/
|
|
48
|
-
export declare const BasicNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<BasicReactWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<Partial<Pick<NVL, "
|
|
48
|
+
export declare const BasicNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<BasicReactWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<Partial<Pick<NVL, "restart" | "destroy" | "addAndUpdateElementsInGraph" | "getSelectedNodes" | "getSelectedRelationships" | "removeNodesWithIds" | "removeRelationshipsWithIds" | "getNodes" | "getRelationships" | "getNodeById" | "getRelationshipById" | "getPositionById" | "getCurrentOptions" | "deselectAll" | "fit" | "resetZoom" | "setRenderer" | "setDisableWebGL" | "pinNode" | "unPinNode" | "setLayout" | "setLayoutOptions" | "getNodesOnScreen" | "getNodePositions" | "setNodePositions" | "isLayoutMoving" | "saveToFile" | "saveToSvg" | "getImageDataUrl" | "getSvgDataUrl" | "saveFullGraphToLargeFile" | "getZoomLimits" | "setZoom" | "getScale" | "getPan" | "getHits" | "getContainer">>>>>;
|
|
@@ -14,6 +14,8 @@ import { useDeepCompareEffect } from '../utils/hooks';
|
|
|
14
14
|
*/
|
|
15
15
|
export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOptions, nvlCallbacks = {}, nvlOptions = {}, positions = [], zoom, pan, onInitializationError, ...nvlEvents }, ref) => {
|
|
16
16
|
const nvlRef = useRef(null);
|
|
17
|
+
const prevZoomRef = useRef(undefined);
|
|
18
|
+
const prevPanRef = useRef(undefined);
|
|
17
19
|
useImperativeHandle(ref, () => {
|
|
18
20
|
const nvlMethods = Object.getOwnPropertyNames(NVL.prototype);
|
|
19
21
|
return nvlMethods.reduce((current, method) => ({
|
|
@@ -30,9 +32,18 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
30
32
|
const containerRef = useRef(null);
|
|
31
33
|
const [currentNodes, setCurrentNodes] = useState(nodes);
|
|
32
34
|
const [currentRels, setCurrentRels] = useState(rels);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
nvlRef.current?.destroy();
|
|
38
|
+
nvlRef.current = null;
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
33
41
|
useEffect(() => {
|
|
34
42
|
let newNvl = null;
|
|
35
|
-
|
|
43
|
+
const hasMinimap = 'minimapContainer' in nvlOptions;
|
|
44
|
+
const minimapContainerIsReady = hasMinimap ? nvlOptions.minimapContainer !== null : true;
|
|
45
|
+
const mainContainerIsReady = containerRef.current !== null;
|
|
46
|
+
if (mainContainerIsReady && minimapContainerIsReady) {
|
|
36
47
|
if (nvlRef.current === null) {
|
|
37
48
|
const combinedOptions = { ...nvlOptions, layoutOptions };
|
|
38
49
|
if (layout !== undefined) {
|
|
@@ -54,12 +65,8 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
|
-
return () => {
|
|
58
|
-
newNvl?.destroy();
|
|
59
|
-
nvlRef.current = null;
|
|
60
|
-
};
|
|
61
68
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
-
}, []);
|
|
69
|
+
}, [containerRef.current, nvlOptions.minimapContainer]);
|
|
63
70
|
useEffect(() => {
|
|
64
71
|
if (nvlRef.current === null) {
|
|
65
72
|
return;
|
|
@@ -122,15 +129,21 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
|
|
|
122
129
|
if (nvlRef.current === null) {
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
125
|
-
|
|
132
|
+
const currentZoom = prevZoomRef.current;
|
|
133
|
+
const currentPan = prevPanRef.current;
|
|
134
|
+
const zoomHasChanged = zoom !== undefined && zoom !== currentZoom;
|
|
135
|
+
const panHasChanged = pan !== undefined && (pan.x !== currentPan?.x || pan.y !== currentPan.y);
|
|
136
|
+
if (zoomHasChanged && panHasChanged) {
|
|
126
137
|
nvlRef.current.setZoomAndPan(zoom, pan.x, pan.y);
|
|
127
138
|
}
|
|
128
|
-
else if (
|
|
139
|
+
else if (zoomHasChanged) {
|
|
129
140
|
nvlRef.current.setZoom(zoom);
|
|
130
141
|
}
|
|
131
|
-
else if (
|
|
142
|
+
else if (panHasChanged) {
|
|
132
143
|
nvlRef.current.setPan(pan.x, pan.y);
|
|
133
144
|
}
|
|
145
|
+
prevZoomRef.current = zoom;
|
|
146
|
+
prevPanRef.current = pan;
|
|
134
147
|
}, [zoom, pan]);
|
|
135
148
|
return _jsx("div", { id: BASIC_WRAPPER_ID, ref: containerRef, style: { height: '100%', outline: '0' }, ...nvlEvents });
|
|
136
149
|
}));
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BasicNvlWrapper } from './basic-wrapper/BasicNvlWrapper';
|
|
2
2
|
import type { BasicReactWrapperProps } from './basic-wrapper/BasicNvlWrapper';
|
|
3
3
|
import { InteractiveNvlWrapper } from './interactive-nvl-wrapper/InteractiveNvlWrapper';
|
|
4
|
-
import type { InteractionOptions, InteractiveNvlWrapperProps, MouseEvent, MouseEventCallbacks } from './interactive-nvl-wrapper/types';
|
|
4
|
+
import type { InteractionOptions, InteractiveNvlWrapperProps, KeyboardEvent, KeyboardEventCallbacks, MouseEvent, MouseEventCallbacks } from './interactive-nvl-wrapper/types';
|
|
5
5
|
import type { StaticPictureWrapperProps } from './static-picture-wrapper/StaticPictureWrapper';
|
|
6
6
|
import { StaticPictureWrapper } from './static-picture-wrapper/StaticPictureWrapper';
|
|
7
7
|
export { BasicNvlWrapper, InteractiveNvlWrapper, StaticPictureWrapper };
|
|
8
|
-
export type { MouseEventCallbacks, MouseEvent, BasicReactWrapperProps, InteractionOptions, InteractiveNvlWrapperProps, StaticPictureWrapperProps };
|
|
8
|
+
export type { MouseEventCallbacks, KeyboardEventCallbacks, MouseEvent, KeyboardEvent, BasicReactWrapperProps, InteractionOptions, InteractiveNvlWrapperProps, StaticPictureWrapperProps };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type NVL from '@neo4j-nvl/base';
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
|
-
import type { InteractionOptions, MouseEventCallbacks } from './types';
|
|
3
|
+
import type { InteractionOptions, KeyboardEventCallbacks, MouseEventCallbacks } from './types';
|
|
4
4
|
interface InteractionHandlersProps {
|
|
5
5
|
nvlRef: MutableRefObject<NVL | null>;
|
|
6
6
|
mouseEventCallbacks: MouseEventCallbacks;
|
|
7
|
+
keyboardEventCallbacks: KeyboardEventCallbacks;
|
|
7
8
|
interactionOptions: InteractionOptions;
|
|
8
9
|
}
|
|
9
10
|
export declare const InteractionHandlers: React.FC<InteractionHandlersProps>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BoxSelectInteraction, ClickInteraction, DragNodeInteraction, DrawInteraction, HoverInteraction, LassoInteraction, PanInteraction, ZoomInteraction } from '@neo4j-nvl/interaction-handlers';
|
|
1
|
+
import { BoxSelectInteraction, ClickInteraction, DragNodeInteraction, DrawInteraction, HoverInteraction, KeyboardInteraction, LassoInteraction, PanInteraction, ZoomInteraction } from '@neo4j-nvl/interaction-handlers';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { destroyInteraction, useInteraction } from './hooks';
|
|
4
|
-
export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOptions }) => {
|
|
4
|
+
export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, keyboardEventCallbacks, interactionOptions }) => {
|
|
5
5
|
const hoverInteraction = useRef(null);
|
|
6
6
|
const clickInteraction = useRef(null);
|
|
7
7
|
const panInteraction = useRef(null);
|
|
@@ -10,6 +10,7 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
|
|
|
10
10
|
const drawInteraction = useRef(null);
|
|
11
11
|
const multiSelectInteraction = useRef(null);
|
|
12
12
|
const lassoInteraction = useRef(null);
|
|
13
|
+
const keyboardInteraction = useRef(null);
|
|
13
14
|
useInteraction(HoverInteraction, hoverInteraction, mouseEventCallbacks.onHover, 'onHover', nvlRef, interactionOptions);
|
|
14
15
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onNodeClick, 'onNodeClick', nvlRef, interactionOptions);
|
|
15
16
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onNodeDoubleClick, 'onNodeDoubleClick', nvlRef, interactionOptions);
|
|
@@ -18,9 +19,11 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
|
|
|
18
19
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onRelationshipDoubleClick, 'onRelationshipDoubleClick', nvlRef, interactionOptions);
|
|
19
20
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onRelationshipRightClick, 'onRelationshipRightClick', nvlRef, interactionOptions);
|
|
20
21
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasClick, 'onCanvasClick', nvlRef, interactionOptions);
|
|
22
|
+
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasDoubleClick, 'onCanvasDoubleClick', nvlRef, interactionOptions);
|
|
21
23
|
useInteraction(ClickInteraction, clickInteraction, mouseEventCallbacks.onCanvasRightClick, 'onCanvasRightClick', nvlRef, interactionOptions);
|
|
22
24
|
useInteraction(PanInteraction, panInteraction, mouseEventCallbacks.onPan, 'onPan', nvlRef, interactionOptions);
|
|
23
25
|
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoom, 'onZoom', nvlRef, interactionOptions);
|
|
26
|
+
useInteraction(ZoomInteraction, zoomInteraction, mouseEventCallbacks.onZoomAndPan, 'onZoomAndPan', nvlRef, interactionOptions);
|
|
24
27
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDrag, 'onDrag', nvlRef, interactionOptions);
|
|
25
28
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDragStart, 'onDragStart', nvlRef, interactionOptions);
|
|
26
29
|
useInteraction(DragNodeInteraction, dragNodeInteraction, mouseEventCallbacks.onDragEnd, 'onDragEnd', nvlRef, interactionOptions);
|
|
@@ -31,6 +34,15 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
|
|
|
31
34
|
useInteraction(BoxSelectInteraction, multiSelectInteraction, mouseEventCallbacks.onBoxSelect, 'onBoxSelect', nvlRef, interactionOptions);
|
|
32
35
|
useInteraction(LassoInteraction, lassoInteraction, mouseEventCallbacks.onLassoStarted, 'onLassoStarted', nvlRef, interactionOptions);
|
|
33
36
|
useInteraction(LassoInteraction, lassoInteraction, mouseEventCallbacks.onLassoSelect, 'onLassoSelect', nvlRef, interactionOptions);
|
|
37
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onKeyDown, 'onKeyDown', nvlRef, interactionOptions);
|
|
38
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onKeyUp, 'onKeyUp', nvlRef, interactionOptions);
|
|
39
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onNodeFocus, 'onNodeFocus', nvlRef, interactionOptions);
|
|
40
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onNodeBlur, 'onNodeBlur', nvlRef, interactionOptions);
|
|
41
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onRelationshipFocus, 'onRelationshipFocus', nvlRef, interactionOptions);
|
|
42
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onRelationshipBlur, 'onRelationshipBlur', nvlRef, interactionOptions);
|
|
43
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onCanvasFocus, 'onCanvasFocus', nvlRef, interactionOptions);
|
|
44
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onCanvasBlur, 'onCanvasBlur', nvlRef, interactionOptions);
|
|
45
|
+
useInteraction(KeyboardInteraction, keyboardInteraction, keyboardEventCallbacks.onContextMenu, 'onContextMenu', nvlRef, interactionOptions);
|
|
34
46
|
useEffect(() => () => {
|
|
35
47
|
destroyInteraction(hoverInteraction);
|
|
36
48
|
destroyInteraction(clickInteraction);
|
|
@@ -40,6 +52,7 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
|
|
|
40
52
|
destroyInteraction(drawInteraction);
|
|
41
53
|
destroyInteraction(multiSelectInteraction);
|
|
42
54
|
destroyInteraction(lassoInteraction);
|
|
55
|
+
destroyInteraction(keyboardInteraction);
|
|
43
56
|
}, []);
|
|
44
57
|
return null;
|
|
45
58
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type NVL from '@neo4j-nvl/base';
|
|
2
2
|
import type { HTMLProps } from 'react';
|
|
3
|
-
import React from 'react';
|
|
4
3
|
import type { InteractiveNvlWrapperProps } from './types';
|
|
5
4
|
/**
|
|
6
5
|
* The interactive React wrapper component contains a collection of interaction handlers by default
|
|
@@ -13,4 +12,4 @@ import type { InteractiveNvlWrapperProps } from './types';
|
|
|
13
12
|
*
|
|
14
13
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_interactive_reactive_wrapperr Interactive React wrapper documentation page}.
|
|
15
14
|
*/
|
|
16
|
-
export declare const InteractiveNvlWrapper:
|
|
15
|
+
export declare const InteractiveNvlWrapper: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Omit<InteractiveNvlWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<NVL>>>;
|
|
@@ -20,7 +20,7 @@ const options = {
|
|
|
20
20
|
*
|
|
21
21
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/react-wrappers/#_interactive_reactive_wrapperr Interactive React wrapper documentation page}.
|
|
22
22
|
*/
|
|
23
|
-
export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOptions, onInitializationError, mouseEventCallbacks = {}, nvlCallbacks = {}, nvlOptions = {}, interactionOptions = options, ...nvlEvents }, nvlRef) => {
|
|
23
|
+
export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOptions, onInitializationError, mouseEventCallbacks = {}, keyboardEventCallbacks = {}, nvlCallbacks = {}, nvlOptions = {}, interactionOptions = options, ...nvlEvents }, nvlRef) => {
|
|
24
24
|
const newNvlRef = useRef(null);
|
|
25
25
|
const myNvlRef = nvlRef ?? newNvlRef;
|
|
26
26
|
const [isNvlInitialized, setIsNvlInitialized] = useState(false);
|
|
@@ -42,5 +42,5 @@ export const InteractiveNvlWrapper = memo(forwardRef(({ nodes, rels, layout, lay
|
|
|
42
42
|
}
|
|
43
43
|
handleInitialization();
|
|
44
44
|
}
|
|
45
|
-
}, layout: layout, layoutOptions: layoutOptions, onInitializationError: handleInitializationError, ...nvlEvents }), setupInteractions && (_jsx(InteractionHandlers, { nvlRef: myNvlRef, mouseEventCallbacks: mouseEventCallbacks, interactionOptions: interactionOptions }))] }));
|
|
45
|
+
}, layout: layout, layoutOptions: layoutOptions, onInitializationError: handleInitializationError, ...nvlEvents }), setupInteractions && (_jsx(InteractionHandlers, { nvlRef: myNvlRef, mouseEventCallbacks: mouseEventCallbacks, keyboardEventCallbacks: keyboardEventCallbacks, interactionOptions: interactionOptions }))] }));
|
|
46
46
|
}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type NVL from '@neo4j-nvl/base';
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
|
-
import type {
|
|
4
|
-
export declare const destroyInteraction: (interactionRef: MutableRefObject<
|
|
5
|
-
export declare const useInteraction: (Interaction:
|
|
3
|
+
import type { Interaction, InteractionModule, InteractionOptions, KeyboardEvent, MouseEvent } from './types';
|
|
4
|
+
export declare const destroyInteraction: (interactionRef: MutableRefObject<Interaction | null>) => void;
|
|
5
|
+
export declare const useInteraction: (Interaction: InteractionModule, interactionRef: MutableRefObject<Interaction | null>, callback: ((...args: unknown[]) => void) | boolean | undefined, eventName: MouseEvent | KeyboardEvent, nvlRef: MutableRefObject<NVL | null>, interactionOptions: InteractionOptions) => void;
|
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
import type { BoxSelectInteraction, BoxSelectInteractionCallbacks, BoxSelectInteractionOptions, ClickInteraction, ClickInteractionCallbacks, ClickInteractionOptions, DragNodeInteraction, DragNodeInteractionCallbacks, DrawInteraction, DrawInteractionCallbacks, DrawInteractionOptions, HoverInteraction, HoverInteractionCallbacks, HoverInteractionOptions, LassoInteraction, LassoInteractionCallbacks, LassoInteractionOptions, PanInteraction, PanInteractionCallbacks, PanInteractionOptions, ZoomInteraction, ZoomInteractionCallbacks } from '@neo4j-nvl/interaction-handlers';
|
|
1
|
+
import type { BoxSelectInteraction, BoxSelectInteractionCallbacks, BoxSelectInteractionOptions, ClickInteraction, ClickInteractionCallbacks, ClickInteractionOptions, DragNodeInteraction, DragNodeInteractionCallbacks, DrawInteraction, DrawInteractionCallbacks, DrawInteractionOptions, HoverInteraction, HoverInteractionCallbacks, HoverInteractionOptions, KeyboardInteraction, KeyboardInteractionCallbacks, KeyboardInteractionOptions, LassoInteraction, LassoInteractionCallbacks, LassoInteractionOptions, PanInteraction, PanInteractionCallbacks, PanInteractionOptions, ZoomInteraction, ZoomInteractionCallbacks, ZoomInteractionOptions } from '@neo4j-nvl/interaction-handlers';
|
|
2
2
|
import type { BasicReactWrapperProps } from '../basic-wrapper/BasicNvlWrapper';
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
3
|
+
export type InteractionModule = typeof HoverInteraction | typeof ClickInteraction | typeof PanInteraction | typeof ZoomInteraction | typeof DragNodeInteraction | typeof DrawInteraction | typeof BoxSelectInteraction | typeof LassoInteraction | typeof KeyboardInteraction;
|
|
4
|
+
export type Interaction = HoverInteraction | ClickInteraction | PanInteraction | ZoomInteraction | DragNodeInteraction | DrawInteraction | BoxSelectInteraction | LassoInteraction | KeyboardInteraction;
|
|
5
5
|
/**
|
|
6
6
|
* Collection of mouse event callbacks that can be used with
|
|
7
7
|
* the {@link InteractiveNvlWrapper} component.
|
|
8
8
|
*/
|
|
9
9
|
export type MouseEventCallbacks = ClickInteractionCallbacks & DragNodeInteractionCallbacks & HoverInteractionCallbacks & PanInteractionCallbacks & ZoomInteractionCallbacks & BoxSelectInteractionCallbacks & DrawInteractionCallbacks & LassoInteractionCallbacks;
|
|
10
|
+
/**
|
|
11
|
+
* Collection of keyboard event callbacks that can be used with
|
|
12
|
+
* the {@link InteractiveNvlWrapper} component.
|
|
13
|
+
*/
|
|
14
|
+
export type KeyboardEventCallbacks = KeyboardInteractionCallbacks;
|
|
10
15
|
/**
|
|
11
16
|
* Collection of interaction options that can be used with
|
|
12
17
|
* the {@link InteractiveNvlWrapper} component.
|
|
13
18
|
*/
|
|
14
|
-
export type InteractionOptions = ClickInteractionOptions & BoxSelectInteractionOptions & HoverInteractionOptions & PanInteractionOptions & LassoInteractionOptions & DrawInteractionOptions;
|
|
19
|
+
export type InteractionOptions = ClickInteractionOptions & BoxSelectInteractionOptions & HoverInteractionOptions & PanInteractionOptions & ZoomInteractionOptions & LassoInteractionOptions & DrawInteractionOptions & KeyboardInteractionOptions;
|
|
15
20
|
/**
|
|
16
21
|
* The events that can be passed to the {@link MouseEventCallbacks} object
|
|
17
22
|
* to turn on/off certain events for the {@link InteractiveNvlWrapper} component.
|
|
18
23
|
*/
|
|
19
24
|
export type MouseEvent = keyof MouseEventCallbacks;
|
|
25
|
+
export type KeyboardEvent = keyof KeyboardEventCallbacks;
|
|
20
26
|
/**
|
|
21
27
|
* The properties that can be passed to the {@link InteractiveNvlWrapper} component.
|
|
22
28
|
*/
|
|
23
29
|
export interface InteractiveNvlWrapperProps extends BasicReactWrapperProps {
|
|
24
30
|
/** {@link MouseEventCallbacks} containing functions for callbacks on certain actions */
|
|
25
31
|
mouseEventCallbacks?: MouseEventCallbacks;
|
|
32
|
+
/** {@link KeyboardEventCallbacks} containing functions for callbacks on keyboard actions */
|
|
33
|
+
keyboardEventCallbacks?: KeyboardEventCallbacks;
|
|
26
34
|
/** {@link InteractionOptions} for the underlying interaction handlers */
|
|
27
35
|
interactionOptions?: InteractionOptions;
|
|
28
36
|
}
|
|
@@ -9,14 +9,31 @@ export type StaticPictureWrapperProps = {
|
|
|
9
9
|
rels: Relationship[];
|
|
10
10
|
/** Options to customize the NVL instance. */
|
|
11
11
|
nvlOptions?: NvlOptions;
|
|
12
|
-
/** The width of the static picture. */
|
|
12
|
+
/** The width of the static picture in pixels. */
|
|
13
13
|
width?: number;
|
|
14
|
-
/** The height of the static picture. */
|
|
14
|
+
/** The height of the static picture in pixels. */
|
|
15
15
|
height?: number;
|
|
16
|
+
/** The format of the static picture: 'png' or 'svg'.
|
|
17
|
+
* Defaults to 'png'. */
|
|
18
|
+
format?: 'png' | 'svg';
|
|
16
19
|
};
|
|
17
20
|
/**
|
|
18
21
|
* A React component that creates a static picture of a graph using NVL.
|
|
19
22
|
* This component is useful for generating static visualizations of graphs without requiring user interaction.
|
|
20
|
-
*
|
|
23
|
+
* The component automatically fits all nodes in the viewport before capturing the image
|
|
24
|
+
* NVL is automatically destroyed after capturing the image to free up resources.
|
|
25
|
+
*
|
|
26
|
+
* @param props - The component properties
|
|
27
|
+
* @returns An img element displaying the graph visualization, or null during rendering
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <StaticPictureWrapper
|
|
32
|
+
* nodes={nodes}
|
|
33
|
+
* rels={relationships}
|
|
34
|
+
* width={500}
|
|
35
|
+
* height={500}
|
|
36
|
+
* />
|
|
37
|
+
* ```
|
|
21
38
|
*/
|
|
22
|
-
export declare const StaticPictureWrapper: ({ nodes, rels, nvlOptions, width, height }: StaticPictureWrapperProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
39
|
+
export declare const StaticPictureWrapper: ({ nodes, rels, nvlOptions, width, height, format }: StaticPictureWrapperProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -4,9 +4,23 @@ import { useEffect, useState } from 'react';
|
|
|
4
4
|
/**
|
|
5
5
|
* A React component that creates a static picture of a graph using NVL.
|
|
6
6
|
* This component is useful for generating static visualizations of graphs without requiring user interaction.
|
|
7
|
-
*
|
|
7
|
+
* The component automatically fits all nodes in the viewport before capturing the image
|
|
8
|
+
* NVL is automatically destroyed after capturing the image to free up resources.
|
|
9
|
+
*
|
|
10
|
+
* @param props - The component properties
|
|
11
|
+
* @returns An img element displaying the graph visualization, or null during rendering
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <StaticPictureWrapper
|
|
16
|
+
* nodes={nodes}
|
|
17
|
+
* rels={relationships}
|
|
18
|
+
* width={500}
|
|
19
|
+
* height={500}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
8
22
|
*/
|
|
9
|
-
export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500, height = 500 }) => {
|
|
23
|
+
export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500, height = 500, format = 'png' }) => {
|
|
10
24
|
const [imgSrc, setImgSrc] = useState();
|
|
11
25
|
useEffect(() => {
|
|
12
26
|
const div = document.createElement('div');
|
|
@@ -17,16 +31,27 @@ export const StaticPictureWrapper = ({ nodes, rels, nvlOptions = {}, width = 500
|
|
|
17
31
|
myNvl.fit(myNvl.getNodes().map((n) => n.id));
|
|
18
32
|
},
|
|
19
33
|
onZoomTransitionDone: () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
const fetchDataUrl = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const dataUrl = format === 'svg' ? await myNvl.getSvgDataUrl() : myNvl.getImageDataUrl();
|
|
37
|
+
setImgSrc(dataUrl);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error('Failed to generate image data URL', err);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
myNvl.destroy();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
void fetchDataUrl();
|
|
24
49
|
}
|
|
25
50
|
});
|
|
26
51
|
return () => {
|
|
27
52
|
myNvl?.destroy();
|
|
28
53
|
};
|
|
29
54
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
-
}, [nodes, rels, width, height]);
|
|
55
|
+
}, [nodes, rels, width, height, format]);
|
|
31
56
|
return imgSrc !== undefined ? _jsx("img", { src: imgSrc, width: width, height: height, alt: "Graph" }) : null;
|
|
32
57
|
};
|
package/lib/utils/constants.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const BASIC_WRAPPER_ID
|
|
2
|
-
export const INTERACTIVE_WRAPPER_ID
|
|
1
|
+
export declare const BASIC_WRAPPER_ID = "NVL_basic-wrapper";
|
|
2
|
+
export declare const INTERACTIVE_WRAPPER_ID = "NVL_interactive-wrapper";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0-01fc2de1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"homepage": "https://neo4j.com/docs/nvl/current/",
|
|
6
6
|
"license": "SEE LICENSE IN 'LICENSE.txt'",
|
|
@@ -27,21 +27,22 @@
|
|
|
27
27
|
"lib"
|
|
28
28
|
],
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@testing-library/dom": "
|
|
31
|
-
"@testing-library/jest-dom": "
|
|
32
|
-
"@testing-library/react": "
|
|
33
|
-
"@types/lodash": "4.
|
|
34
|
-
"@types/react": "
|
|
35
|
-
"react": "
|
|
36
|
-
"react-dom": "
|
|
30
|
+
"@testing-library/dom": "10.4.1",
|
|
31
|
+
"@testing-library/jest-dom": "5.17.0",
|
|
32
|
+
"@testing-library/react": "16.3.0",
|
|
33
|
+
"@types/lodash": "4.17.21",
|
|
34
|
+
"@types/react": "18.3.27",
|
|
35
|
+
"react": "19.2.1",
|
|
36
|
+
"react-dom": "19.2.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@neo4j-nvl/base": "1.
|
|
40
|
-
"@neo4j-nvl/interaction-handlers": "1.
|
|
41
|
-
"lodash": "4.
|
|
39
|
+
"@neo4j-nvl/base": "1.1.0-01fc2de1",
|
|
40
|
+
"@neo4j-nvl/interaction-handlers": "1.1.0-01fc2de1",
|
|
41
|
+
"lodash": "4.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"react": "
|
|
45
|
-
"react-dom": "
|
|
46
|
-
}
|
|
44
|
+
"react": "18.0.0 || ^19.0.0",
|
|
45
|
+
"react-dom": "18.0.0 || ^19.0.0"
|
|
46
|
+
},
|
|
47
|
+
"stableVersion": "1.1.0"
|
|
47
48
|
}
|