@centreon/ui 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.5 → 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.5",
3
+ "version": "24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.7",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -101,18 +101,6 @@ export const WithCustomLinks: Story = {
101
101
  render: StandaloneTreeTemplate
102
102
  };
103
103
 
104
- export const WithResetButton: Story = {
105
- args: {
106
- children: SimpleContent,
107
- node: {
108
- height: 90,
109
- width: 90
110
- },
111
- tree: simpleData
112
- },
113
- render: StandaloneTreeTemplate
114
- };
115
-
116
104
  export const WithComplexData: Story = {
117
105
  args: {
118
106
  children: ComplexContent,
@@ -6,7 +6,7 @@ import { isNil } from 'ramda';
6
6
 
7
7
  import { useDeepCompare } from '../../utils';
8
8
 
9
- import { margins, nodeMargins } from './constants';
9
+ import { nodeMargins } from './constants';
10
10
  import { BaseProp, Node, TreeProps } from './models';
11
11
  import { updateNodeFromTree } from './utils';
12
12
  import Links from './Links';
@@ -81,12 +81,14 @@ export const Tree = <TData extends BaseProp>({
81
81
  );
82
82
 
83
83
  return (
84
- <Group left={node.width / 2} top={margins.top}>
84
+ <Group left={node.width / 2}>
85
85
  <VisxTree
86
+ left={0}
86
87
  nodeSize={[node.width + nodeMargins.y, node.height + nodeMargins.x]}
87
88
  root={hierarchy(formattedTree, getExpanded)}
88
89
  separation={() => 1}
89
90
  size={[containerWidth, containerHeight]}
91
+ top={0}
90
92
  >
91
93
  {(subTree) => (
92
94
  <Group left={origin.x} top={origin.y}>
@@ -287,14 +287,9 @@ export const moreComplexData: Node<ComplexData> = {
287
287
  { data: { id: 30, name: 'Indicator 30', status: 'ok' } },
288
288
  { data: { id: 31, name: 'Indicator 31', status: 'ok' } },
289
289
  { data: { id: 32, name: 'Indicator 32', status: 'ok' } },
290
- { data: { id: 33, name: 'Indicator 33', status: 'ok' } },
291
- { data: { id: 34, name: 'Indicator 34', status: 'ok' } },
292
- { data: { id: 35, name: 'Indicator 35', status: 'ok' } },
293
- { data: { id: 36, name: 'Indicator 36', status: 'ok' } },
294
- { data: { id: 37, name: 'Indicator 37', status: 'ok' } },
295
- { data: { id: 38, name: 'Indicator 38', status: 'ok' } }
290
+ { data: { id: 33, name: 'Indicator 33', status: 'ok' } }
296
291
  ],
297
- data: { count: 10, id: 0, name: 'ok', status: 'ok' }
292
+ data: { count: 2, id: 0, name: 'ok', status: 'ok' }
298
293
  }
299
294
  ],
300
295
  data: {
@@ -1,8 +1,12 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { scaleLinear } from '@visx/scale';
4
+
1
5
  import { minimapScale, radius } from './constants';
2
6
  import { UseMinimapProps, useMinimap } from './useMinimap';
3
7
  import { useZoomStyles } from './Zoom.styles';
4
8
 
5
- interface Props extends Omit<UseMinimapProps, 'minimapScale'> {
9
+ interface Props extends Omit<UseMinimapProps, 'minimapScale' | 'scale'> {
6
10
  children: JSX.Element;
7
11
  contentClientRect: {
8
12
  height: number;
@@ -21,39 +25,75 @@ const Minimap = ({
21
25
  }: Props): JSX.Element => {
22
26
  const { classes } = useZoomStyles();
23
27
 
24
- const { transformTo, move, zoomInOut } = useMinimap({
28
+ const yMinimapScale = useMemo(
29
+ () => contentClientRect.height / zoom.transformMatrix.scaleY / height,
30
+ [contentClientRect.height, height]
31
+ );
32
+ const xMinimapScale = useMemo(
33
+ () => contentClientRect.width / zoom.transformMatrix.scaleX / width,
34
+ [contentClientRect.width, width]
35
+ );
36
+
37
+ const scale = Math.max(yMinimapScale, xMinimapScale);
38
+ const invertedScale = 1 / scale;
39
+ const scaleToUse = (invertedScale > 1 ? 1 : invertedScale) || 1;
40
+
41
+ const { move, zoomInOut, dragStart, dragEnd } = useMinimap({
25
42
  height,
26
43
  isDraggingFromContainer,
27
44
  minimapScale,
45
+ scale: (1 / scale > 1 ? 1 : scale) || 1,
28
46
  width,
29
47
  zoom
30
48
  });
31
49
 
50
+ const finalHeight = height;
51
+ const finalWidth = width;
52
+
53
+ const additionalScale = scaleLinear({
54
+ clamp: true,
55
+ domain: [contentClientRect.height, 0],
56
+ range: [0, 0.05]
57
+ });
58
+
32
59
  return (
33
60
  <g className={classes.minimap} clipPath="url(#zoom-clip)">
34
61
  <rect
35
62
  className={classes.minimapBackground}
36
- height={Math.max(contentClientRect?.height || 0, height)}
37
- rx={radius}
38
- width={Math.max(contentClientRect?.width || 0, width)}
39
- />
40
- {children}
41
- <rect
42
- className={classes.minimapZoom}
43
- fillOpacity={0.2}
44
- height={height}
63
+ height={finalHeight}
45
64
  rx={radius}
46
- transform={zoom.toStringInvert()}
47
- width={width}
65
+ width={finalWidth}
48
66
  />
67
+ <g
68
+ style={{
69
+ transform: `scale(${scaleToUse - additionalScale(contentClientRect.height - height) / 2})`
70
+ }}
71
+ >
72
+ {children}
73
+ <g
74
+ style={{
75
+ transform: `translate(0px, ${contentClientRect.height / 10}px)`
76
+ }}
77
+ >
78
+ <rect
79
+ className={classes.minimapZoom}
80
+ fillOpacity={0.2}
81
+ height={height}
82
+ rx={radius}
83
+ transform={zoom.toStringInvert()}
84
+ width={width}
85
+ />
86
+ </g>
87
+ </g>
49
88
  <rect
50
89
  data-testid="minimap-interaction"
51
90
  fill="transparent"
52
- height={Math.max(contentClientRect?.height || 0, height)}
91
+ height={finalHeight}
53
92
  rx={radius}
54
- width={Math.max(contentClientRect?.width || 0, width)}
55
- onMouseDown={transformTo}
93
+ width={finalWidth}
94
+ onMouseDown={dragStart}
56
95
  onMouseMove={move}
96
+ onMouseUp={dragEnd}
57
97
  onWheel={zoomInOut}
58
98
  />
59
99
  </g>
@@ -6,16 +6,36 @@ const content = (
6
6
  </g>
7
7
  );
8
8
 
9
- const labels = {
10
- clear: 'Clear'
11
- };
9
+ const contentWithMultipleShapes = (
10
+ <g style={{ transform: 'translate(0px, -200px)' }}>
11
+ <g style={{ transform: 'translate(300px, 150px)' }}>
12
+ <circle fill="blue" r={50} stroke="black" />
13
+ </g>
14
+ <g style={{ transform: 'translate(600px, 500px)' }}>
15
+ <circle fill="green" r={70} />
16
+ </g>
17
+ <g style={{ transform: 'translate(150px, 600px)' }}>
18
+ <circle fill="red" r={70} />
19
+ </g>
20
+ </g>
21
+ );
12
22
 
13
- const initialize = ({ showMinimap }): void => {
23
+ interface Props {
24
+ minimapPosition?;
25
+ showMinimap: boolean;
26
+ useMultipleShapes?: boolean;
27
+ }
28
+
29
+ const initialize = ({
30
+ showMinimap,
31
+ minimapPosition,
32
+ useMultipleShapes
33
+ }: Props): void => {
14
34
  cy.mount({
15
35
  Component: (
16
36
  <div style={{ height: '400px', width: '100%' }}>
17
- <Zoom labels={labels} showMinimap={showMinimap}>
18
- {content}
37
+ <Zoom minimapPosition={minimapPosition} showMinimap={showMinimap}>
38
+ {useMultipleShapes ? contentWithMultipleShapes : content}
19
39
  </Zoom>
20
40
  </div>
21
41
  )
@@ -102,7 +122,7 @@ describe('Zoom', () => {
102
122
  .should('have.attr', 'transform')
103
123
  .and('include', '1.1, 0, 0, 1.1');
104
124
 
105
- cy.contains('Clear').click();
125
+ cy.findByTestId('clear').click();
106
126
 
107
127
  cy.get('g[transform="matrix(1, 0, 0, 1, 0, 0)"]');
108
128
 
@@ -146,57 +166,44 @@ describe('Zoom', () => {
146
166
  cy.makeSnapshot();
147
167
  });
148
168
 
149
- it('moves the view when the minimap is clicked', () => {
169
+ it('moves the view when the mouse is hover the content with the corresponding button pressed down', () => {
150
170
  initialize({ showMinimap: true });
151
171
 
152
172
  cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
153
173
  cy.get('svg').should('have.attr', 'height', '400');
154
174
 
155
- cy.findByTestId('minimap-interaction').click(20, 20);
175
+ cy.findByTestId('zoom-container')
176
+ .trigger('mousedown', 400, 200)
177
+ .trigger('mousemove', 600, 200);
156
178
 
157
179
  cy.findByTestId('zoom-content').should(
158
180
  'have.attr',
159
181
  'transform',
160
- 'matrix(1, 0, 0, 1, -100, -100)'
182
+ 'matrix(1, 0, 0, 1, 200, 0)'
161
183
  );
162
184
 
163
185
  cy.makeSnapshot();
164
186
  });
165
187
 
166
- it('moves the view when the mouse is hover the minimap with the corresponding button pressed down', () => {
167
- initialize({ showMinimap: true });
188
+ it('displays the minimap in the bottom right when the prop to the corresponding value', () => {
189
+ initialize({ minimapPosition: 'bottom-right', showMinimap: true });
168
190
 
169
191
  cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
170
192
  cy.get('svg').should('have.attr', 'height', '400');
171
193
 
172
- cy.findByTestId('minimap-interaction')
173
- .realMouseDown()
174
- .realMouseMove(40, 40);
175
-
176
- cy.findByTestId('zoom-content').should(
177
- 'have.attr',
178
- 'transform',
179
- 'matrix(1, 0, 0, 1, -640, -200)'
180
- );
181
-
182
194
  cy.makeSnapshot();
183
195
  });
184
196
 
185
- it('moves the view when the mouse is hover the content with the corresponding button pressed down', () => {
186
- initialize({ showMinimap: true });
197
+ it('applies a scale down on the minimap when the content is higher than the original height', () => {
198
+ initialize({ showMinimap: true, useMultipleShapes: true });
187
199
 
188
200
  cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
189
201
  cy.get('svg').should('have.attr', 'height', '400');
190
202
 
191
- cy.findByTestId('zoom-container')
192
- .trigger('mousedown', 400, 200)
193
- .trigger('mousemove', 600, 200);
194
-
195
- cy.findByTestId('zoom-content').should(
196
- 'have.attr',
197
- 'transform',
198
- 'matrix(1, 0, 0, 1, 200, 0)'
199
- );
203
+ cy.findByTestId('minimap-interaction')
204
+ .parent()
205
+ .find('g')
206
+ .should('have.attr', 'style', 'transform: scale(0.684211);');
200
207
 
201
208
  cy.makeSnapshot();
202
209
  });
@@ -4,6 +4,10 @@ import Zoom, { ZoomProps } from './Zoom';
4
4
 
5
5
  const meta: Meta<typeof Zoom> = {
6
6
  argTypes: {
7
+ minimapPosition: {
8
+ control: 'select',
9
+ options: ['top-left', 'top-right', 'bottom-left', 'bottom-right']
10
+ },
7
11
  scaleMax: {
8
12
  control: { max: 16, min: 0.4, step: 0.2, type: 'range' }
9
13
  },
@@ -21,8 +25,16 @@ export default meta;
21
25
  type Story = StoryObj<typeof Zoom>;
22
26
 
23
27
  const content = (
24
- <g style={{ transform: 'translate(300px, 150px)' }}>
25
- <circle fill="blue" r={50} stroke="black" />
28
+ <g style={{ transform: 'translate(0px, -200px)' }}>
29
+ <g style={{ transform: 'translate(300px, 150px)' }}>
30
+ <circle fill="blue" r={50} stroke="black" />
31
+ </g>
32
+ <g style={{ transform: 'translate(600px, 400px)' }}>
33
+ <circle fill="green" r={70} />
34
+ </g>
35
+ <g style={{ transform: 'translate(150px, 800px)' }}>
36
+ <circle fill="red" r={70} />
37
+ </g>
26
38
  </g>
27
39
  );
28
40
 
@@ -32,22 +44,25 @@ const Template = ({ children, ...args }: ZoomProps): JSX.Element => (
32
44
  </div>
33
45
  );
34
46
 
35
- const labels = {
36
- clear: 'Clear'
47
+ export const WithoutMinimap: Story = {
48
+ args: {
49
+ children: content
50
+ },
51
+ render: Template
37
52
  };
38
53
 
39
- export const WithoutMinimap: Story = {
54
+ export const WithMinimap: Story = {
40
55
  args: {
41
56
  children: content,
42
- labels
57
+ showMinimap: true
43
58
  },
44
59
  render: Template
45
60
  };
46
61
 
47
- export const WithMinimap: Story = {
62
+ export const WithMinimapPosition: Story = {
48
63
  args: {
49
64
  children: content,
50
- labels,
65
+ minimapPosition: 'bottom-right',
51
66
  showMinimap: true
52
67
  },
53
68
  render: Template
@@ -7,26 +7,53 @@ import { minimapScale } from './constants';
7
7
  export const useZoomStyles = makeStyles()((theme) => ({
8
8
  actions: {
9
9
  backgroundColor: alpha(theme.palette.background.paper, 0.8),
10
- borderRadius: `0 ${theme.shape.borderRadius}px 0 0`,
11
- bottom: 0,
12
10
  display: 'flex',
13
- flexDirection: 'row',
14
- gap: theme.spacing(1),
15
- left: 0,
16
- padding: theme.spacing(1),
11
+ flexDirection: 'row'
12
+ },
13
+ actionsAndZoom: {
14
+ '&[data-position="bottom-left"]': {
15
+ alignItems: 'flex-start',
16
+ bottom: 0,
17
+ flexDirection: 'column-reverse',
18
+ left: 0
19
+ },
20
+ '&[data-position="bottom-right"]': {
21
+ alignItems: 'flex-end',
22
+ bottom: 0,
23
+ flexDirection: 'column-reverse',
24
+ right: 0
25
+ },
26
+ '&[data-position="top-left"]': {
27
+ alignItems: 'flex-start',
28
+ flexDirection: 'column',
29
+ left: 0,
30
+ top: 0
31
+ },
32
+ '&[data-position="top-right"]': {
33
+ alignItems: 'flex-end',
34
+ flexDirection: 'column',
35
+ right: 0,
36
+ top: 0
37
+ },
38
+ display: 'flex',
17
39
  position: 'absolute',
18
- transition: 'top 0.15s ease-out'
40
+ width: 'fit-content'
19
41
  },
20
42
  minimap: {
21
43
  transform: `scale(${minimapScale})`
22
44
  },
23
45
  minimapBackground: {
24
- fill: theme.palette.background.default,
25
- stroke: theme.palette.background.default
46
+ fill: theme.palette.background.paper,
47
+ stroke: theme.palette.background.paper
48
+ },
49
+ minimapContainer: {
50
+ border: `1px solid ${theme.palette.divider}`,
51
+ borderRadius: theme.shape.borderRadius
26
52
  },
27
- minimapContainer: { left: 0, position: 'absolute', top: 0 },
28
53
  minimapZoom: {
29
- fill: theme.palette.text.primary
54
+ fill: theme.palette.primary.main,
55
+ stroke: theme.palette.primary.main,
56
+ strokeWidth: 10
30
57
  },
31
58
  movingZone: {
32
59
  transition: 'transform 0.1s linear'
@@ -1,26 +1,13 @@
1
- import { useState, useRef, useEffect } from 'react';
2
-
3
1
  import { Zoom as VisxZoom } from '@visx/zoom';
4
- import { RectClipPath } from '@visx/clip-path';
5
- import { Group } from '@visx/group';
6
- import { equals, type } from 'ramda';
7
-
8
- import ZoomInIcon from '@mui/icons-material/Add';
9
- import ZoomOutIcon from '@mui/icons-material/Remove';
10
2
 
11
3
  import { ParentSize } from '../..';
12
- import { Button, IconButton } from '../Button';
13
4
 
14
- import { useZoomStyles } from './Zoom.styles';
15
- import Minimap from './Minimap';
16
- import { useZoom } from './useZoom';
17
- import { minimapScale } from './constants';
5
+ import ZoomContent from './ZoomContent';
6
+ import { MinimapPosition } from './models';
18
7
 
19
8
  export interface ZoomProps {
20
9
  children: JSX.Element | (({ width, height }) => JSX.Element);
21
- labels: {
22
- clear: string;
23
- };
10
+ minimapPosition?: MinimapPosition;
24
11
  scaleMax?: number;
25
12
  scaleMin?: number;
26
13
  showMinimap?: boolean;
@@ -34,49 +21,14 @@ const initialTransform = {
34
21
  translateX: 0,
35
22
  translateY: 0
36
23
  };
37
- const radius = 30;
38
24
 
39
25
  const Zoom = ({
40
26
  children,
41
27
  scaleMin = 0.5,
42
28
  scaleMax = 4,
43
29
  showMinimap = false,
44
- labels
30
+ minimapPosition = 'top-left'
45
31
  }: ZoomProps): JSX.Element => {
46
- const { classes } = useZoomStyles();
47
-
48
- const [contentClientRect, setContentClientRect] = useState<{
49
- height: number;
50
- width: number;
51
- } | null>(null);
52
- const contentRef = useRef<SVGGElement | null>(null);
53
-
54
- const { move, dragEnd, dragStart, isDragging } = useZoom();
55
-
56
- const resizeObserver = new ResizeObserver(() => {
57
- setContentClientRect({
58
- height: contentRef.current?.getBoundingClientRect().height || 0,
59
- width: contentRef.current?.getBoundingClientRect().width || 0
60
- });
61
- });
62
-
63
- useEffect(() => {
64
- if (contentRef.current) {
65
- resizeObserver.disconnect();
66
- setContentClientRect({
67
- height: contentRef.current?.getBoundingClientRect().height || 0,
68
- width: contentRef.current?.getBoundingClientRect().width || 0
69
- });
70
- resizeObserver.observe(contentRef.current);
71
- }
72
-
73
- return () => {
74
- resizeObserver.disconnect();
75
- };
76
- }, [contentRef.current]);
77
-
78
- const isChildrenObject = equals(type(children), 'Object');
79
-
80
32
  return (
81
33
  <ParentSize>
82
34
  {({ width, height }) => (
@@ -90,83 +42,15 @@ const Zoom = ({
90
42
  width={width}
91
43
  >
92
44
  {(zoom) => (
93
- <div style={{ position: 'relative' }}>
94
- <svg
95
- className={classes.svg}
96
- data-is-grabbing={isDragging}
97
- data-testid="zoom-container"
98
- height={height}
99
- width={width}
100
- onMouseDown={dragStart(zoom)}
101
- onMouseMove={move(zoom)}
102
- onMouseUp={dragEnd}
103
- onWheel={zoom.handleWheel}
104
- >
105
- <RectClipPath
106
- height={Math.max(contentClientRect?.height || 0, height)}
107
- id="zoom-clip"
108
- rx={radius}
109
- width={Math.max(contentClientRect?.width || 0, width)}
110
- />
111
- <g
112
- data-testid="zoom-content"
113
- ref={contentRef}
114
- transform={zoom.toString()}
115
- >
116
- {isChildrenObject ? children : children({ height, width })}
117
- </g>
118
- </svg>
119
- <svg
120
- className={classes.minimapContainer}
121
- data-testid="minimap"
122
- height={
123
- Math.max(contentClientRect?.height || 0, height) *
124
- minimapScale
125
- }
126
- width={
127
- Math.max(contentClientRect?.width || 0, width) * minimapScale
128
- }
129
- >
130
- {showMinimap && contentClientRect && (
131
- <Minimap
132
- contentClientRect={contentClientRect}
133
- height={height}
134
- isDraggingFromContainer={isDragging}
135
- width={width}
136
- zoom={zoom}
137
- >
138
- <Group left={0} top={contentClientRect.height / 10}>
139
- {isChildrenObject
140
- ? children
141
- : children({ height, width })}
142
- </Group>
143
- </Minimap>
144
- )}
145
- </svg>
146
- <div className={classes.actions}>
147
- <IconButton
148
- data-testid="zoom in"
149
- icon={<ZoomInIcon />}
150
- size="small"
151
- onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
152
- />
153
- <IconButton
154
- data-testid="zoom out"
155
- icon={<ZoomOutIcon />}
156
- size="small"
157
- onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
158
- />
159
-
160
- <Button
161
- data-testid="clear"
162
- size="small"
163
- variant="ghost"
164
- onClick={zoom.clear}
165
- >
166
- {labels.clear}
167
- </Button>
168
- </div>
169
- </div>
45
+ <ZoomContent
46
+ height={height}
47
+ minimapPosition={minimapPosition}
48
+ showMinimap={showMinimap}
49
+ width={width}
50
+ zoom={zoom}
51
+ >
52
+ {children}
53
+ </ZoomContent>
170
54
  )}
171
55
  </VisxZoom>
172
56
  )}
@@ -0,0 +1,140 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ import { RectClipPath } from '@visx/clip-path';
4
+ import { equals, type } from 'ramda';
5
+ import { Group } from '@visx/group';
6
+ import { ProvidedZoom } from '@visx/zoom/lib/types';
7
+
8
+ import ZoomInIcon from '@mui/icons-material/Add';
9
+ import ZoomOutIcon from '@mui/icons-material/Remove';
10
+ import ReplayIcon from '@mui/icons-material/Replay';
11
+
12
+ import { IconButton } from '../Button';
13
+
14
+ import { minimapScale, radius } from './constants';
15
+ import { useZoom } from './useZoom';
16
+ import { useZoomStyles } from './Zoom.styles';
17
+ import Minimap from './Minimap';
18
+ import { MinimapPosition, ZoomState } from './models';
19
+
20
+ export interface Props {
21
+ children: JSX.Element | (({ width, height }) => JSX.Element);
22
+ height: number;
23
+ minimapPosition: MinimapPosition;
24
+ showMinimap?: boolean;
25
+ width: number;
26
+ zoom: ProvidedZoom<SVGSVGElement> & ZoomState;
27
+ }
28
+
29
+ const ZoomContent = ({
30
+ zoom,
31
+ width,
32
+ height,
33
+ children,
34
+ showMinimap,
35
+ minimapPosition
36
+ }: Props): JSX.Element => {
37
+ const { classes } = useZoomStyles();
38
+ const contentRef = useRef<SVGGElement | null>(null);
39
+ const [contentClientRect, setContentClientRect] = useState<{
40
+ height: number;
41
+ width: number;
42
+ } | null>(null);
43
+
44
+ const resizeObserver = new ResizeObserver(() => {
45
+ setContentClientRect({
46
+ height: (contentRef.current as SVGGElement).getBoundingClientRect()
47
+ .height,
48
+ width: (contentRef.current as SVGGElement).getBoundingClientRect().width
49
+ });
50
+ });
51
+
52
+ useEffect(() => {
53
+ if (contentRef.current) {
54
+ resizeObserver.disconnect();
55
+ resizeObserver.observe(contentRef.current);
56
+ }
57
+
58
+ return () => {
59
+ resizeObserver.disconnect();
60
+ };
61
+ }, [contentRef.current]);
62
+
63
+ const { move, dragEnd, dragStart, isDragging } = useZoom();
64
+
65
+ const isChildrenObject = equals(type(children), 'Object');
66
+
67
+ return (
68
+ <div style={{ position: 'relative' }}>
69
+ <svg
70
+ className={classes.svg}
71
+ data-is-grabbing={isDragging}
72
+ data-testid="zoom-container"
73
+ height={height}
74
+ width={width}
75
+ onMouseDown={dragStart(zoom)}
76
+ onMouseMove={move(zoom)}
77
+ onMouseUp={dragEnd}
78
+ onWheel={zoom.handleWheel}
79
+ >
80
+ <RectClipPath
81
+ height={Math.max(contentClientRect?.height || 0, height)}
82
+ id="zoom-clip"
83
+ rx={radius}
84
+ width={Math.max(contentClientRect?.width || 0, width)}
85
+ />
86
+ <g
87
+ data-testid="zoom-content"
88
+ ref={contentRef}
89
+ transform={zoom.toString()}
90
+ >
91
+ {isChildrenObject ? children : children({ height, width })}
92
+ </g>
93
+ </svg>
94
+ <div className={classes.actionsAndZoom} data-position={minimapPosition}>
95
+ {showMinimap && contentClientRect && (
96
+ <svg
97
+ className={classes.minimapContainer}
98
+ data-testid="minimap"
99
+ height={height * minimapScale}
100
+ width={width * minimapScale}
101
+ >
102
+ <Minimap
103
+ contentClientRect={contentClientRect}
104
+ height={height}
105
+ isDraggingFromContainer={isDragging}
106
+ width={width}
107
+ zoom={zoom}
108
+ >
109
+ <Group left={0} top={contentClientRect.height / 10}>
110
+ {isChildrenObject ? children : children({ height, width })}
111
+ </Group>
112
+ </Minimap>
113
+ </svg>
114
+ )}
115
+ <div className={classes.actions}>
116
+ <IconButton
117
+ data-testid="zoom in"
118
+ icon={<ZoomInIcon />}
119
+ size="small"
120
+ onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
121
+ />
122
+ <IconButton
123
+ data-testid="zoom out"
124
+ icon={<ZoomOutIcon />}
125
+ size="small"
126
+ onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
127
+ />
128
+ <IconButton
129
+ data-testid="clear"
130
+ icon={<ReplayIcon />}
131
+ size="small"
132
+ onClick={zoom.reset}
133
+ />
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ };
139
+
140
+ export default ZoomContent;
@@ -1,2 +1,2 @@
1
- export const radius = 30;
1
+ export const radius = 16;
2
2
  export const minimapScale = 0.2;
@@ -4,7 +4,6 @@ import type {
4
4
  TouchEvent as ReactTouchEvent
5
5
  } from 'react';
6
6
 
7
- import { has } from 'ramda';
8
7
  import { Point } from '@visx/point';
9
8
 
10
9
  type EventType =
@@ -44,16 +43,7 @@ const getXAndYFromEvent = (event?: EventType): PointCoords => {
44
43
  };
45
44
  }
46
45
 
47
- const boundingClientRect = has('getBoundingClientRect', event.target)
48
- ? event.target.getBoundingClientRect()
49
- : null;
50
-
51
- if (!boundingClientRect) return { ...DEFAULT_POINT };
52
-
53
- return {
54
- x: boundingClientRect.x + boundingClientRect.width / 2,
55
- y: boundingClientRect.y + boundingClientRect.height / 2
56
- };
46
+ return { ...DEFAULT_POINT };
57
47
  };
58
48
 
59
49
  export const localPoint = (event: EventType): PointCoords | null => {
@@ -8,3 +8,9 @@ export interface ZoomState {
8
8
  translateY: number;
9
9
  };
10
10
  }
11
+
12
+ export type MinimapPosition =
13
+ | 'top-left'
14
+ | 'top-right'
15
+ | 'bottom-left'
16
+ | 'bottom-right';
@@ -1,7 +1,8 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useState } from 'react';
2
2
 
3
- import { ProvidedZoom } from '@visx/zoom/lib/types';
4
- import { equals, gt } from 'ramda';
3
+ import { ProvidedZoom, Translate } from '@visx/zoom/lib/types';
4
+ import { equals, gt, isNil, pick } from 'ramda';
5
+ import { Point } from '@visx/point';
5
6
 
6
7
  import { ZoomState } from './models';
7
8
 
@@ -9,11 +10,14 @@ export interface UseMinimapProps {
9
10
  height: number;
10
11
  isDraggingFromContainer: boolean;
11
12
  minimapScale: number;
13
+ scale: number;
12
14
  width: number;
13
15
  zoom: ProvidedZoom<SVGSVGElement> & ZoomState;
14
16
  }
15
17
 
16
18
  interface UseMinimapState {
19
+ dragEnd: (e) => void;
20
+ dragStart: (e) => void;
17
21
  move: (e) => void;
18
22
  transformTo: (e) => void;
19
23
  zoomInOut: (e) => void;
@@ -24,26 +28,36 @@ export const useMinimap = ({
24
28
  height,
25
29
  zoom,
26
30
  minimapScale,
27
- isDraggingFromContainer
31
+ isDraggingFromContainer,
32
+ scale
28
33
  }: UseMinimapProps): UseMinimapState => {
34
+ const [startPoint, setStartPoint] = useState<Pick<Point, 'x' | 'y'> | null>(
35
+ null
36
+ );
37
+ const [startTranslate, setStartTranslate] = useState<Translate | null>(null);
38
+
29
39
  const getMatrixPoint = useCallback(
30
40
  (event): { x: number; y: number } => {
41
+ const hasScale = scale > 1;
31
42
  const point = {
32
43
  x: event.nativeEvent.offsetX * (1 / minimapScale),
33
44
  y: event.nativeEvent.offsetY * (1 / minimapScale)
34
45
  };
35
46
 
47
+ const dx = -(point.x * zoom.transformMatrix.scaleX - width / 2);
48
+ const dy = -(point.y * zoom.transformMatrix.scaleY - height / 2);
49
+
36
50
  return {
37
- x: -(point.x * zoom.transformMatrix.scaleX - width / 2),
38
- y: -(point.y * zoom.transformMatrix.scaleY - height / 2)
51
+ x: !hasScale ? dx : dx * scale - width / 2,
52
+ y: !hasScale ? dy : dy * scale - height / 2
39
53
  };
40
54
  },
41
- [zoom.transformMatrix]
55
+ [zoom.transformMatrix, scale, width, height, minimapScale]
42
56
  );
43
57
 
44
58
  const transformTo = useCallback(
45
59
  (e): void => {
46
- if (!equals(e.nativeEvent.which, 1)) {
60
+ if (!isNil(e.nativeEvent.which) && !equals(e.nativeEvent.which, 1)) {
47
61
  return;
48
62
  }
49
63
  const { x, y } = getMatrixPoint(e);
@@ -53,17 +67,43 @@ export const useMinimap = ({
53
67
  translateY: y
54
68
  });
55
69
  },
56
- [zoom.transformMatrix]
70
+ [zoom.transformMatrix, scale]
57
71
  );
58
72
 
73
+ const dragStart = (e): void => {
74
+ if (
75
+ (!equals(e.buttons, 0) && !equals(e.nativeEvent.which, 1)) ||
76
+ isDraggingFromContainer
77
+ ) {
78
+ return;
79
+ }
80
+ setStartPoint(getMatrixPoint(e));
81
+ setStartTranslate(pick(['translateX', 'translateY'], zoom.transformMatrix));
82
+ };
83
+
84
+ const dragEnd = (): void => {
85
+ setStartPoint(null);
86
+ setStartTranslate(null);
87
+ };
88
+
59
89
  const move = useCallback(
60
90
  (e): void => {
61
- if (!equals(e.nativeEvent.which, 1) || isDraggingFromContainer) {
91
+ if (!startPoint || !startTranslate) {
62
92
  return;
63
93
  }
64
- transformTo(e);
94
+ const { x, y } = getMatrixPoint(e);
95
+
96
+ const diffX = startPoint.x - x;
97
+ const diffY = startPoint.y - y;
98
+
99
+ zoom.setTransformMatrix({
100
+ ...zoom.transformMatrix,
101
+ translateX: startTranslate.translateX - diffX,
102
+ translateY: startTranslate.translateY - diffY
103
+ });
65
104
  },
66
- [zoom.transformMatrix, isDraggingFromContainer]
105
+
106
+ [zoom.transformMatrix, isDraggingFromContainer, scale, startPoint]
67
107
  );
68
108
 
69
109
  const zoomInOut = useCallback(
@@ -87,6 +127,8 @@ export const useMinimap = ({
87
127
  );
88
128
 
89
129
  return {
130
+ dragEnd,
131
+ dragStart,
90
132
  move,
91
133
  transformTo,
92
134
  zoomInOut