@neo4j-nvl/react 1.1.0 → 1.2.0-bf56a927

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 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.2.0] - 2026-05-27
6
+
7
+ This 1.2.0 release introduces a new keyboard interaction handler and SVG support for the static image wrapper React component. It also includes several performance improvements for clustering and text/icon rendering, as well as several bug fixes.
8
+
9
+ ### Added
10
+ * SVG format support in the static image wrapper.
11
+ * Keyboard interaction handlers.
12
+
13
+ ### Changed
14
+ * Boost performance of graph clustering for force directed layouts.
15
+ * Optimize Canvas rendering performance by caching expensive operations.
16
+ * Deprecate Cytoscape support.
17
+
18
+ ### Fixed
19
+ * Fix Arabic text rendering issues to ensure characters stay connected.
20
+ * Prevent arrow ends from bouncing by removing position rounding.
21
+ * Fix issue where D3 layout would occasionally fail to display.
22
+ * Fix node position loss during a restart when the device pixel ratio changes.
23
+ * Add missing support for relationship overlay icons within the SVG renderer.
24
+ * Fix bug where reactive zoom values were not consistently applied.
25
+ * Correct the pinch-to-zoom speed scaling on the minimap.
26
+ * Add safety guards against non-finite values to prevent runtime rendering errors.
27
+ * Ensure fit targets respect `maxZoom` constraints even when omitted from `zoomOptions`.
28
+ * Force canvas repaint when icon images finish asynchronous loading.
29
+
5
30
  ## [1.1.0] - 2026-02-16
6
31
 
7
32
  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.
@@ -2,7 +2,7 @@ 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 React from 'react';
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');
@@ -43,10 +43,20 @@ describe('BasicNvlWrapper', () => {
43
43
  test('successfully re-initialises NVL when using React StrictMode', () => {
44
44
  const destroy = jest.fn();
45
45
  jest.spyOn(NVL.prototype, 'destroy').mockImplementation(destroy);
46
- render(_jsx(React.StrictMode, { children: _jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [] }) }) }));
46
+ render(_jsx(StrictMode, { children: _jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [] }) }) }));
47
47
  expect(NVL).toHaveBeenCalledTimes(2);
48
48
  expect(destroy).toHaveBeenCalledTimes(1);
49
49
  });
50
+ test('reapplies zoom and pan when re-initialising NVL with React StrictMode', () => {
51
+ const destroy = jest.fn();
52
+ jest.spyOn(NVL.prototype, 'destroy').mockImplementation(destroy);
53
+ render(_jsx(StrictMode, { children: _jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5, pan: { x: 10, y: 20 } }) }) }));
54
+ expect(NVL).toHaveBeenCalledTimes(2);
55
+ expect(destroy).toHaveBeenCalledTimes(1);
56
+ expect(setZoomAndPan).toHaveBeenCalledTimes(2);
57
+ expect(setZoomAndPan).toHaveBeenNthCalledWith(1, 1.5, 10, 20);
58
+ expect(setZoomAndPan).toHaveBeenNthCalledWith(2, 1.5, 10, 20);
59
+ });
50
60
  test('calls setZoom when zoom property is provided', () => {
51
61
  render(_jsx("div", { children: _jsx(BasicNvlWrapper, { nodes: [], rels: [], zoom: 1.5 }) }));
52
62
  expect(setZoom).toHaveBeenCalledWith(1.5);
@@ -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 React, { act, createRef } from 'react';
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');
@@ -42,6 +42,10 @@ LassoInteraction.prototype.callbackMap = new Map();
42
42
  LassoInteraction.prototype.updateCallback = jest.fn();
43
43
  LassoInteraction.prototype.removeCallback = jest.fn();
44
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();
45
49
  let mockDestroy;
46
50
  let destroyHoverInteraction;
47
51
  let destroyPanInteraction;
@@ -70,6 +74,7 @@ describe('InteractiveNvlWrapper', () => {
70
74
  jest.spyOn(DrawInteraction.prototype, 'destroy').mockImplementation(jest.fn());
71
75
  jest.spyOn(BoxSelectInteraction.prototype, 'destroy').mockImplementation(jest.fn());
72
76
  jest.spyOn(LassoInteraction.prototype, 'destroy').mockImplementation(jest.fn());
77
+ jest.spyOn(KeyboardInteraction.prototype, 'destroy').mockImplementation(jest.fn());
73
78
  });
74
79
  afterEach(() => {
75
80
  jest.clearAllMocks();
@@ -110,6 +115,7 @@ describe('InteractiveNvlWrapper', () => {
110
115
  expect(ZoomInteraction).not.toHaveBeenCalled();
111
116
  expect(BoxSelectInteraction).not.toHaveBeenCalled();
112
117
  expect(LassoInteraction).not.toHaveBeenCalled();
118
+ expect(KeyboardInteraction).not.toHaveBeenCalled();
113
119
  });
114
120
  test.each([
115
121
  {
@@ -253,6 +259,48 @@ describe('InteractiveNvlWrapper', () => {
253
259
  expect(InteractionClass).toHaveBeenCalledWith(myNvlRef.current, options);
254
260
  expect(InteractionClass).toHaveBeenCalledTimes(1);
255
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);
303
+ });
256
304
  test('does not attempt to initialize interaction handlers if NVL container is not available', () => {
257
305
  const myNvlRef = createRef();
258
306
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -280,7 +328,7 @@ describe('InteractiveNvlWrapper', () => {
280
328
  });
281
329
  test('successfully re-initialises NVL and active interaction handlers when using React StrictMode', () => {
282
330
  const myNvlRef = createRef();
283
- render(_jsx(React.StrictMode, { children: _jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, mouseEventCallbacks: {
331
+ render(_jsx(StrictMode, { children: _jsx(InteractiveNvlWrapper, { nodes: [], rels: [], ref: myNvlRef, mouseEventCallbacks: {
284
332
  onHover: true
285
333
  }, interactionOptions: {
286
334
  drawShadowOnHover: true
@@ -299,6 +347,19 @@ describe('InteractiveNvlWrapper', () => {
299
347
  expect(ZoomInteraction).not.toHaveBeenCalled();
300
348
  expect(BoxSelectInteraction).not.toHaveBeenCalled();
301
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);
302
363
  });
303
364
  test('also calls custom callbacks on initialization', () => {
304
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, "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" | "saveFullGraphToLargeFile" | "getZoomLimits" | "setZoom" | "getScale" | "getPan" | "getHits" | "getContainer">>>>>;
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">>>>>;
@@ -36,6 +36,8 @@ export const BasicNvlWrapper = memo(forwardRef(({ nodes, rels, layout, layoutOpt
36
36
  return () => {
37
37
  nvlRef.current?.destroy();
38
38
  nvlRef.current = null;
39
+ prevZoomRef.current = undefined;
40
+ prevPanRef.current = undefined;
39
41
  };
40
42
  }, []);
41
43
  useEffect(() => {
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);
@@ -33,6 +34,15 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
33
34
  useInteraction(BoxSelectInteraction, multiSelectInteraction, mouseEventCallbacks.onBoxSelect, 'onBoxSelect', nvlRef, interactionOptions);
34
35
  useInteraction(LassoInteraction, lassoInteraction, mouseEventCallbacks.onLassoStarted, 'onLassoStarted', nvlRef, interactionOptions);
35
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);
36
46
  useEffect(() => () => {
37
47
  destroyInteraction(hoverInteraction);
38
48
  destroyInteraction(clickInteraction);
@@ -42,6 +52,7 @@ export const InteractionHandlers = ({ nvlRef, mouseEventCallbacks, interactionOp
42
52
  destroyInteraction(drawInteraction);
43
53
  destroyInteraction(multiSelectInteraction);
44
54
  destroyInteraction(lassoInteraction);
55
+ destroyInteraction(keyboardInteraction);
45
56
  }, []);
46
57
  return null;
47
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: React.MemoExoticComponent<React.ForwardRefExoticComponent<Omit<InteractiveNvlWrapperProps & HTMLProps<HTMLDivElement>, "ref"> & React.RefAttributes<NVL>>>;
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 { InteractionOptions, MouseEvent, MouseInteraction, MouseInteractionModule } from './types';
4
- export declare const destroyInteraction: (interactionRef: MutableRefObject<MouseInteraction | null>) => void;
5
- export declare const useInteraction: (Interaction: MouseInteractionModule, interactionRef: MutableRefObject<MouseInteraction | null>, callback: ((...args: unknown[]) => void) | boolean | undefined, eventName: MouseEvent, nvlRef: MutableRefObject<NVL | null>, interactionOptions: InteractionOptions) => void;
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, ZoomInteractionOptions } 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 MouseInteractionModule = typeof HoverInteraction | typeof ClickInteraction | typeof PanInteraction | typeof ZoomInteraction | typeof DragNodeInteraction | typeof DrawInteraction | typeof BoxSelectInteraction | typeof LassoInteraction;
4
- export type MouseInteraction = HoverInteraction | ClickInteraction | PanInteraction | ZoomInteraction | DragNodeInteraction | DrawInteraction | BoxSelectInteraction | LassoInteraction;
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 & ZoomInteractionOptions & 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
- * NVL is destroyed after capturing the image to free up resources.
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
- * NVL is destroyed after capturing the image to free up resources.
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
- setImgSrc(myNvl.getImageDataUrl());
21
- setTimeout(() => {
22
- myNvl.destroy();
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
  };
@@ -1,2 +1,2 @@
1
- export const BASIC_WRAPPER_ID: "NVL_basic-wrapper";
2
- export const INTERACTIVE_WRAPPER_ID: "NVL_interactive-wrapper";
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.1.0",
3
+ "version": "1.2.0-bf56a927",
4
4
  "main": "lib/index.js",
5
5
  "homepage": "https://neo4j.com/docs/nvl/current/",
6
6
  "license": "SEE LICENSE IN 'LICENSE.txt'",
@@ -36,12 +36,13 @@
36
36
  "react-dom": "19.2.1"
37
37
  },
38
38
  "dependencies": {
39
- "@neo4j-nvl/base": "1.1.0",
40
- "@neo4j-nvl/interaction-handlers": "1.1.0",
41
- "lodash": "4.17.23"
39
+ "@neo4j-nvl/base": "1.2.0-bf56a927",
40
+ "@neo4j-nvl/interaction-handlers": "1.2.0-bf56a927",
41
+ "lodash": "4.18.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "18.0.0 || ^19.0.0",
45
45
  "react-dom": "18.0.0 || ^19.0.0"
46
- }
46
+ },
47
+ "stableVersion": "1.2.0"
47
48
  }