@neo4j-nvl/interaction-handlers 1.0.0 → 1.1.0-151c52c8

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.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
 
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,322 @@
1
+ import NVL from '@neo4j-nvl/base';
2
+ import '@testing-library/jest-dom';
3
+ import { KeyboardInteraction } from '../interaction-handlers/keyboard-interaction';
4
+ jest.mock('@neo4j-nvl/layout-workers');
5
+ describe('KeyboardInteraction', () => {
6
+ let keyboardInteraction;
7
+ let myNVL;
8
+ const callbackMock = jest.fn();
9
+ beforeEach(() => {
10
+ myNVL = new NVL(document.createElement('div'), [
11
+ { id: '0', x: 100, y: 100 },
12
+ { id: '1', x: 200, y: 200 }
13
+ ], [{ id: '10', from: '0', to: '1' }], { disableWebGL: true, initialZoom: 1, layout: 'free', renderer: 'webgl' });
14
+ keyboardInteraction = new KeyboardInteraction(myNVL);
15
+ });
16
+ afterEach(() => {
17
+ keyboardInteraction.destroy();
18
+ myNVL.destroy();
19
+ callbackMock.mockReset();
20
+ });
21
+ test('sets tabindex on the container if not already present', () => {
22
+ expect(myNVL.getContainer().getAttribute('tabindex')).toBe('0');
23
+ });
24
+ test('does not override existing tabindex', () => {
25
+ keyboardInteraction.destroy();
26
+ const container = myNVL.getContainer();
27
+ container.setAttribute('tabindex', '-1');
28
+ keyboardInteraction = new KeyboardInteraction(myNVL);
29
+ expect(container.getAttribute('tabindex')).toBe('-1');
30
+ });
31
+ test('removes tabindex on destroy only if it was added by the interaction', () => {
32
+ keyboardInteraction.destroy();
33
+ expect(myNVL.getContainer().getAttribute('tabindex')).toBeNull();
34
+ });
35
+ test('does not remove tabindex on destroy if it was pre-existing', () => {
36
+ keyboardInteraction.destroy();
37
+ const container = myNVL.getContainer();
38
+ container.setAttribute('tabindex', '-1');
39
+ const interaction = new KeyboardInteraction(myNVL);
40
+ interaction.destroy();
41
+ expect(container.getAttribute('tabindex')).toBe('-1');
42
+ });
43
+ test('keydown event triggers onKeyDown callback', () => {
44
+ keyboardInteraction.updateCallback('onKeyDown', callbackMock);
45
+ const event = new KeyboardEvent('keydown', { key: 'a' });
46
+ myNVL.getContainer().dispatchEvent(event);
47
+ expect(callbackMock).toHaveBeenCalledWith(event, undefined);
48
+ expect(callbackMock).toHaveBeenCalledTimes(1);
49
+ });
50
+ test('keyup event triggers onKeyUp callback', () => {
51
+ keyboardInteraction.updateCallback('onKeyUp', callbackMock);
52
+ const event = new KeyboardEvent('keyup', { key: 'a' });
53
+ myNVL.getContainer().dispatchEvent(event);
54
+ expect(callbackMock).toHaveBeenCalledWith(event, undefined);
55
+ expect(callbackMock).toHaveBeenCalledTimes(1);
56
+ });
57
+ test('focusin event triggers onCanvasFocus callback', () => {
58
+ keyboardInteraction.updateCallback('onCanvasFocus', callbackMock);
59
+ const event = new FocusEvent('focusin');
60
+ myNVL.getContainer().dispatchEvent(event);
61
+ expect(callbackMock).toHaveBeenCalledWith(event);
62
+ expect(callbackMock).toHaveBeenCalledTimes(1);
63
+ });
64
+ test('focusout event triggers onCanvasBlur callback', () => {
65
+ keyboardInteraction.updateCallback('onCanvasBlur', callbackMock);
66
+ const event = new FocusEvent('focusout');
67
+ myNVL.getContainer().dispatchEvent(event);
68
+ expect(callbackMock).toHaveBeenCalledWith(event);
69
+ expect(callbackMock).toHaveBeenCalledTimes(1);
70
+ });
71
+ describe('automatic navigation via key mappings', () => {
72
+ test('Enter key enters the graph and focuses the first element', () => {
73
+ keyboardInteraction.updateCallback('onNodeFocus', callbackMock);
74
+ const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true });
75
+ myNVL.getContainer().dispatchEvent(event);
76
+ expect(callbackMock).toHaveBeenCalledTimes(1);
77
+ expect(callbackMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), event);
78
+ });
79
+ test('Enter key does nothing when an element is already focused', () => {
80
+ const nodeFocusMock = jest.fn();
81
+ keyboardInteraction.updateCallback('onNodeFocus', nodeFocusMock);
82
+ const container = myNVL.getContainer();
83
+ // Enter the graph first
84
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
85
+ expect(nodeFocusMock).toHaveBeenCalledTimes(1);
86
+ // Press Enter again — should not navigate
87
+ nodeFocusMock.mockClear();
88
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
89
+ expect(nodeFocusMock).not.toHaveBeenCalled();
90
+ });
91
+ test('Tab does nothing when the graph has not been entered', () => {
92
+ keyboardInteraction.updateCallback('onNodeFocus', callbackMock);
93
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
94
+ expect(callbackMock).not.toHaveBeenCalled();
95
+ });
96
+ test('Tab key navigates focus forward after entering the graph', () => {
97
+ const nodeFocusMock = jest.fn();
98
+ keyboardInteraction.updateCallback('onNodeFocus', nodeFocusMock);
99
+ const container = myNVL.getContainer();
100
+ // Enter the graph first
101
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
102
+ expect(nodeFocusMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
103
+ // Tab to next element
104
+ const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
105
+ container.dispatchEvent(event);
106
+ expect(nodeFocusMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '1' }), event);
107
+ });
108
+ test('Shift+Tab does nothing when the graph has not been entered', () => {
109
+ keyboardInteraction.updateCallback('onRelationshipFocus', callbackMock);
110
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true }));
111
+ expect(callbackMock).not.toHaveBeenCalled();
112
+ });
113
+ test('Shift+Tab navigates focus backward after entering the graph', () => {
114
+ const nodeFocusMock = jest.fn();
115
+ const relFocusMock = jest.fn();
116
+ keyboardInteraction.updateCallback('onNodeFocus', nodeFocusMock);
117
+ keyboardInteraction.updateCallback('onRelationshipFocus', relFocusMock);
118
+ const container = myNVL.getContainer();
119
+ // Enter the graph first
120
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
121
+ // Shift+Tab to go backward
122
+ const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true });
123
+ container.dispatchEvent(event);
124
+ expect(relFocusMock).toHaveBeenCalledWith(expect.objectContaining({ id: '10' }), event);
125
+ });
126
+ test('Tab cycles through nodes and relationships', () => {
127
+ const nodeFocusMock = jest.fn();
128
+ const relFocusMock = jest.fn();
129
+ keyboardInteraction.updateCallback('onNodeFocus', nodeFocusMock);
130
+ keyboardInteraction.updateCallback('onRelationshipFocus', relFocusMock);
131
+ const container = myNVL.getContainer();
132
+ // Enter the graph: node '0'
133
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
134
+ expect(nodeFocusMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
135
+ // First Tab: node '1'
136
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
137
+ expect(nodeFocusMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '1' }), expect.any(KeyboardEvent));
138
+ // Second Tab: relationship '10'
139
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
140
+ expect(relFocusMock).toHaveBeenCalledWith(expect.objectContaining({ id: '10' }), expect.any(KeyboardEvent));
141
+ });
142
+ test('Tab wraps around from last to first element', () => {
143
+ const nodeFocusMock = jest.fn();
144
+ keyboardInteraction.updateCallback('onNodeFocus', nodeFocusMock);
145
+ keyboardInteraction.updateCallback('onRelationshipFocus', jest.fn());
146
+ const container = myNVL.getContainer();
147
+ // Enter the graph, then navigate to the last element (relationship '10')
148
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
149
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
150
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
151
+ // Wrap around to first element
152
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
153
+ expect(nodeFocusMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
154
+ });
155
+ test('Tab fires blur callback when moving away from a node', () => {
156
+ const nodeBlurMock = jest.fn();
157
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
158
+ keyboardInteraction.updateCallback('onNodeBlur', nodeBlurMock);
159
+ const container = myNVL.getContainer();
160
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
161
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
162
+ expect(nodeBlurMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
163
+ });
164
+ test('Tab fires onRelationshipBlur when moving away from a relationship', () => {
165
+ const relBlurMock = jest.fn();
166
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
167
+ keyboardInteraction.updateCallback('onRelationshipFocus', jest.fn());
168
+ keyboardInteraction.updateCallback('onRelationshipBlur', relBlurMock);
169
+ const container = myNVL.getContainer();
170
+ // Enter the graph, then navigate to relationship
171
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
172
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
173
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
174
+ // Move away from relationship
175
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
176
+ expect(relBlurMock).toHaveBeenCalledWith(expect.objectContaining({ id: '10' }), expect.any(KeyboardEvent));
177
+ });
178
+ test('does nothing when there are no elements', () => {
179
+ keyboardInteraction.destroy();
180
+ myNVL.destroy();
181
+ myNVL = new NVL(document.createElement('div'), [], [], {
182
+ disableWebGL: true,
183
+ initialZoom: 1,
184
+ layout: 'free',
185
+ renderer: 'webgl'
186
+ });
187
+ keyboardInteraction = new KeyboardInteraction(myNVL);
188
+ keyboardInteraction.updateCallback('onNodeFocus', callbackMock);
189
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
190
+ expect(callbackMock).not.toHaveBeenCalled();
191
+ });
192
+ test('Escape clears element focus but keeps canvas focused', () => {
193
+ const nodeBlurMock = jest.fn();
194
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
195
+ keyboardInteraction.updateCallback('onNodeBlur', nodeBlurMock);
196
+ const container = myNVL.getContainer();
197
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
198
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
199
+ expect(nodeBlurMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
200
+ expect(keyboardInteraction.getFocused()).toBeUndefined();
201
+ });
202
+ test('Escape does nothing when nothing is focused', () => {
203
+ keyboardInteraction.updateCallback('onNodeBlur', callbackMock);
204
+ keyboardInteraction.updateCallback('onRelationshipBlur', callbackMock);
205
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
206
+ expect(callbackMock).not.toHaveBeenCalled();
207
+ });
208
+ test('Shift+F10 fires onContextMenu with focused element', () => {
209
+ const contextMenuMock = jest.fn();
210
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
211
+ keyboardInteraction.updateCallback('onContextMenu', contextMenuMock);
212
+ const container = myNVL.getContainer();
213
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
214
+ const event = new KeyboardEvent('keydown', { key: 'F10', shiftKey: true, bubbles: true });
215
+ container.dispatchEvent(event);
216
+ expect(contextMenuMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), event);
217
+ });
218
+ test('Shift+F10 fires onContextMenu with undefined when nothing is focused', () => {
219
+ const contextMenuMock = jest.fn();
220
+ keyboardInteraction.updateCallback('onContextMenu', contextMenuMock);
221
+ const event = new KeyboardEvent('keydown', { key: 'F10', shiftKey: true, bubbles: true });
222
+ myNVL.getContainer().dispatchEvent(event);
223
+ expect(contextMenuMock).toHaveBeenCalledWith(undefined, event);
224
+ });
225
+ });
226
+ describe('custom key mappings', () => {
227
+ test('respects custom enterGraphKey', () => {
228
+ keyboardInteraction.destroy();
229
+ keyboardInteraction = new KeyboardInteraction(myNVL, { enterGraphKey: ['Space'] });
230
+ keyboardInteraction.updateCallback('onNodeFocus', callbackMock);
231
+ // Default Enter should not enter the graph
232
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
233
+ expect(callbackMock).not.toHaveBeenCalled();
234
+ // Custom key should enter the graph
235
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Space', bubbles: true }));
236
+ expect(callbackMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
237
+ });
238
+ test('respects custom navigateForwardKey', () => {
239
+ keyboardInteraction.destroy();
240
+ keyboardInteraction = new KeyboardInteraction(myNVL, { navigateForwardKey: ['ArrowDown'] });
241
+ keyboardInteraction.updateCallback('onNodeFocus', callbackMock);
242
+ const container = myNVL.getContainer();
243
+ // Enter the graph first
244
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
245
+ // Custom forward key should navigate to the next element
246
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
247
+ expect(callbackMock).toHaveBeenLastCalledWith(expect.objectContaining({ id: '1' }), expect.any(KeyboardEvent));
248
+ });
249
+ test('respects custom navigateBackwardKey', () => {
250
+ keyboardInteraction.destroy();
251
+ keyboardInteraction = new KeyboardInteraction(myNVL, { navigateBackwardKey: ['ArrowUp'] });
252
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
253
+ keyboardInteraction.updateCallback('onRelationshipFocus', callbackMock);
254
+ const container = myNVL.getContainer();
255
+ // Enter the graph first
256
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
257
+ // Custom backward key should navigate to the previous (last) element
258
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
259
+ expect(callbackMock).toHaveBeenCalledWith(expect.objectContaining({ id: '10' }), expect.any(KeyboardEvent));
260
+ });
261
+ test('respects custom exitGraphKey', () => {
262
+ keyboardInteraction.destroy();
263
+ keyboardInteraction = new KeyboardInteraction(myNVL, { exitGraphKey: ['q'] });
264
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
265
+ keyboardInteraction.updateCallback('onNodeBlur', callbackMock);
266
+ const container = myNVL.getContainer();
267
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
268
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'q', bubbles: true }));
269
+ expect(callbackMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), expect.any(KeyboardEvent));
270
+ });
271
+ test('respects custom contextMenuKey', () => {
272
+ keyboardInteraction.destroy();
273
+ keyboardInteraction = new KeyboardInteraction(myNVL, { contextMenuKey: ['Ctrl', 'm'] });
274
+ const contextMenuMock = jest.fn();
275
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
276
+ keyboardInteraction.updateCallback('onContextMenu', contextMenuMock);
277
+ const container = myNVL.getContainer();
278
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
279
+ const event = new KeyboardEvent('keydown', { key: 'm', ctrlKey: true, bubbles: true });
280
+ container.dispatchEvent(event);
281
+ expect(contextMenuMock).toHaveBeenCalledWith(expect.objectContaining({ id: '0' }), event);
282
+ });
283
+ });
284
+ test('getFocused returns the currently focused node', () => {
285
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
286
+ expect(keyboardInteraction.getFocused()).toBeUndefined();
287
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
288
+ expect(keyboardInteraction.getFocused()).toEqual(expect.objectContaining({ id: '0' }));
289
+ });
290
+ test('getFocused returns the currently focused relationship', () => {
291
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
292
+ keyboardInteraction.updateCallback('onRelationshipFocus', jest.fn());
293
+ const container = myNVL.getContainer();
294
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
295
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
296
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
297
+ expect(keyboardInteraction.getFocused()).toEqual(expect.objectContaining({ id: '10' }));
298
+ });
299
+ test('onKeyDown callback receives the currently focused element', () => {
300
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
301
+ keyboardInteraction.updateCallback('onKeyDown', callbackMock);
302
+ const container = myNVL.getContainer();
303
+ container.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
304
+ const event = new KeyboardEvent('keydown', { key: 'a', bubbles: true });
305
+ container.dispatchEvent(event);
306
+ expect(callbackMock).toHaveBeenLastCalledWith(event, expect.objectContaining({ id: '0' }));
307
+ });
308
+ test('destroy removes event listeners', () => {
309
+ keyboardInteraction.updateCallback('onKeyDown', callbackMock);
310
+ keyboardInteraction.destroy();
311
+ const event = new KeyboardEvent('keydown', { key: 'Tab' });
312
+ myNVL.getContainer().dispatchEvent(event);
313
+ expect(callbackMock).not.toHaveBeenCalled();
314
+ });
315
+ test('destroy clears focus state', () => {
316
+ keyboardInteraction.updateCallback('onNodeFocus', jest.fn());
317
+ myNVL.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
318
+ expect(keyboardInteraction.getFocused()).toBeDefined();
319
+ keyboardInteraction.destroy();
320
+ expect(keyboardInteraction.getFocused()).toBeUndefined();
321
+ });
322
+ });
@@ -112,4 +112,109 @@ describe('PanInteraction', () => {
112
112
  document.body.dispatchEvent(bodyMouseUpEvent);
113
113
  expect(document.body.style.getPropertyValue('user-select')).toBe('');
114
114
  });
115
+ test('when controlledPan is true, should not update NVL instance', () => {
116
+ panInteraction.destroy();
117
+ myNVL.destroy();
118
+ myNVL = new NVL(document.createElement('div'), [
119
+ { id: '0', x: 10, y: 10 },
120
+ { id: '1', x: 200, y: 200 }
121
+ ], [{ id: '10', from: '0', to: '1' }], { disableWebGL: true, initialZoom: 1, layout: 'free' });
122
+ const panInteractionNoUpdate = new PanInteraction(myNVL, { controlledPan: true });
123
+ panInteractionNoUpdate.updateCallback('onPan', panCallbackMock);
124
+ const initialPan = myNVL.getPan();
125
+ const mouseDownEvent = new MouseEvent('mousedown', {
126
+ clientX: 100,
127
+ clientY: 100
128
+ });
129
+ const mouseMoveEvent = new MouseEvent('mousemove', {
130
+ buttons: 1,
131
+ clientX: 120,
132
+ clientY: 120
133
+ });
134
+ const mouseUpEvent = new MouseEvent('mouseup', {
135
+ clientX: 120,
136
+ clientY: 120
137
+ });
138
+ const container = myNVL.getContainer();
139
+ container.dispatchEvent(mouseDownEvent);
140
+ container.dispatchEvent(mouseMoveEvent);
141
+ container.dispatchEvent(mouseUpEvent);
142
+ return new Promise((resolve) => {
143
+ expect(myNVL.getPan()).toEqual(initialPan);
144
+ expect(panCallbackMock).toHaveBeenCalledTimes(1);
145
+ expect(panCallbackMock).toHaveBeenCalledWith({
146
+ x: initialPan.x - 20,
147
+ y: initialPan.y - 20
148
+ }, mouseMoveEvent);
149
+ panInteractionNoUpdate.destroy();
150
+ resolve();
151
+ });
152
+ });
153
+ test('when controlledPan is false, should update NVL instance normally', () => {
154
+ panInteraction.destroy();
155
+ myNVL.destroy();
156
+ myNVL = new NVL(document.createElement('div'), [
157
+ { id: '0', x: 10, y: 10 },
158
+ { id: '1', x: 200, y: 200 }
159
+ ], [{ id: '10', from: '0', to: '1' }], { disableWebGL: true, initialZoom: 1, layout: 'free' });
160
+ const panInteractionWithUpdate = new PanInteraction(myNVL, { controlledPan: false });
161
+ panInteractionWithUpdate.updateCallback('onPan', panCallbackMock);
162
+ const initialPan = myNVL.getPan();
163
+ const mouseDownEvent = new MouseEvent('mousedown', {
164
+ clientX: 100,
165
+ clientY: 100
166
+ });
167
+ const mouseMoveEvent = new MouseEvent('mousemove', {
168
+ buttons: 1,
169
+ clientX: 120,
170
+ clientY: 120
171
+ });
172
+ const mouseUpEvent = new MouseEvent('mouseup');
173
+ const container = myNVL.getContainer();
174
+ container.dispatchEvent(mouseDownEvent);
175
+ container.dispatchEvent(mouseMoveEvent);
176
+ container.dispatchEvent(mouseUpEvent);
177
+ return new Promise((resolve) => {
178
+ expect(myNVL.getPan()).not.toEqual(initialPan);
179
+ expect(myNVL.getPan()).toEqual({
180
+ x: initialPan.x - 20,
181
+ y: initialPan.y - 20
182
+ });
183
+ expect(panCallbackMock).toHaveBeenCalledTimes(1);
184
+ panInteractionWithUpdate.destroy();
185
+ resolve();
186
+ });
187
+ });
188
+ test('when controlledPan is true, should provide correct callback values for multiple operations', () => {
189
+ panInteraction.destroy();
190
+ myNVL.destroy();
191
+ myNVL = new NVL(document.createElement('div'), [
192
+ { id: '0', x: 10, y: 10 },
193
+ { id: '1', x: 200, y: 200 }
194
+ ], [{ id: '10', from: '0', to: '1' }], { disableWebGL: true, initialZoom: 1, layout: 'free' });
195
+ const panInteractionNoUpdate = new PanInteraction(myNVL, { controlledPan: true });
196
+ panInteractionNoUpdate.updateCallback('onPan', panCallbackMock);
197
+ const initialPan = myNVL.getPan();
198
+ const container = myNVL.getContainer();
199
+ container.dispatchEvent(new MouseEvent('mousedown', { clientX: 100, clientY: 100 }));
200
+ container.dispatchEvent(new MouseEvent('mousemove', { buttons: 1, clientX: 120, clientY: 120 }));
201
+ container.dispatchEvent(new MouseEvent('mouseup'));
202
+ container.dispatchEvent(new MouseEvent('mousedown', { clientX: 120, clientY: 120 }));
203
+ container.dispatchEvent(new MouseEvent('mousemove', { buttons: 1, clientX: 140, clientY: 140 }));
204
+ container.dispatchEvent(new MouseEvent('mouseup'));
205
+ return new Promise((resolve) => {
206
+ expect(panCallbackMock).toHaveBeenCalledTimes(2);
207
+ expect(panCallbackMock).toHaveBeenNthCalledWith(1, {
208
+ x: initialPan.x - 20,
209
+ y: initialPan.y - 20
210
+ }, expect.anything());
211
+ expect(panCallbackMock).toHaveBeenNthCalledWith(2, {
212
+ x: initialPan.x - 20,
213
+ y: initialPan.y - 20
214
+ }, expect.anything());
215
+ expect(myNVL.getPan()).toEqual(initialPan);
216
+ panInteractionNoUpdate.destroy();
217
+ resolve();
218
+ });
219
+ });
115
220
  });