Skip to content

Commit d2941f0

Browse files
authored
feature/MIG-6598 Add contextual node zoom (#20)
* feature/MIG-6598 Add contextual zoom * feature/MIG-6598 Fix zoom threshold to align with existing value * feature/MIG-6598 Fix padding
1 parent 75e3bc5 commit d2941f0

File tree

8 files changed

+109
-19
lines changed

8 files changed

+109
-19
lines changed

src/components/canvas/canvas.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { Node } from '@/components/node/node';
1010
import { useCanvas } from '@/components/canvas/use-canvas';
1111
import { InternalNode } from '@/types/internal';
1212

13+
const MAX_ZOOM = 3;
14+
const MIN_ZOOM = 0.1;
15+
1316
const PRO_OPTIONS: ProOptions = {
1417
hideAttribution: true,
1518
};
@@ -42,6 +45,8 @@ export const Canvas = ({ title, nodes: externalNodes }: Props) => {
4245
<ReactFlow
4346
title={title}
4447
proOptions={PRO_OPTIONS}
48+
maxZoom={MAX_ZOOM}
49+
minZoom={MIN_ZOOM}
4550
nodeTypes={nodeTypes}
4651
nodes={nodes}
4752
onNodesChange={onNodesChange}

src/components/field/field-list.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { spacing } from '@leafygreen-ui/tokens';
44
import { Field } from '@/components/field/field';
55
import { NodeField } from '@/types';
66
import { getPreviewGroupLengths } from '@/utilities/get-preview-group-lengths';
7+
import { DEFAULT_FIELD_PADDING } from '@/utilities/constants';
78

89
const NodeFieldWrapper = styled.div`
9-
padding: ${spacing[200]}px ${spacing[400]}px ${spacing[200]}px ${spacing[400]}px;
10+
padding: ${DEFAULT_FIELD_PADDING}px ${spacing[400]}px;
1011
font-size: 12px;
1112
`;
1213

src/components/field/field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const Field = ({
146146
<FieldWrapper color={getTextColor()}>
147147
<InnerFieldWrapper width={spacing}>
148148
{glyphs.map(glyph => (
149-
<IconWrapper key={glyph} size={11} color={getIconColor(glyph)} glyph={GlyphToIcon[glyph]} />
149+
<IconWrapper key={glyph} color={getIconColor(glyph)} glyph={GlyphToIcon[glyph]} />
150150
))}
151151
</InnerFieldWrapper>
152152
{previewGroupLength ? (

src/components/node/node.stories.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Meta, StoryObj } from '@storybook/react';
2+
import { ReactFlowProvider } from '@xyflow/react';
23

34
import { InternalNode } from '@/types/internal';
45
import { Node } from '@/components/node/node';
@@ -31,9 +32,11 @@ const nodeStory: Meta<typeof Node> = {
3132
component: Node,
3233
decorators: [
3334
Story => (
34-
<div style={{ padding: '100px' }}>
35-
<Story />
36-
</div>
35+
<ReactFlowProvider>
36+
<div style={{ padding: '100px' }}>
37+
<Story />
38+
</div>
39+
</ReactFlowProvider>
3740
),
3841
],
3942
};

src/components/node/node.test.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { screen } from '@testing-library/react';
2-
import { NodeProps } from '@xyflow/react';
2+
import { NodeProps, useViewport } from '@xyflow/react';
33

44
import { render } from '@/mocks/testing-utils';
55
import { InternalNode } from '@/types/internal';
66
import { Node } from '@/components/node/node';
77

8+
vi.mock('@xyflow/react', async () => {
9+
const actual = await vi.importActual<typeof import('@xyflow/react')>('@xyflow/react');
10+
return {
11+
...actual,
12+
useViewport: vi.fn(),
13+
};
14+
});
15+
816
describe('node', () => {
917
const DEFAULT_PROPS: NodeProps<InternalNode> = {
1018
id: 'id',
@@ -21,13 +29,52 @@ describe('node', () => {
2129
positionAbsoluteY: 0,
2230
};
2331

32+
beforeEach(() => {
33+
const mockedViewport = vi.mocked(useViewport);
34+
mockedViewport.mockReturnValue({ zoom: 1, x: 0, y: 0 });
35+
});
36+
2437
it('Should show table node', () => {
25-
render(<Node {...DEFAULT_PROPS} type="table" data={{ title: 'orders', fields: [] }} />);
38+
render(
39+
<Node
40+
{...DEFAULT_PROPS}
41+
type="table"
42+
data={{ title: 'orders', fields: [{ name: 'orderId', type: 'varchar' }] }}
43+
/>,
44+
);
45+
expect(screen.getByRole('img', { name: 'Drag Icon' })).toBeInTheDocument();
2646
expect(screen.getByText('orders')).toBeInTheDocument();
47+
expect(screen.getByText('orderId')).toBeInTheDocument();
48+
expect(screen.getByText('varchar')).toBeInTheDocument();
2749
});
2850

2951
it('Should show collection node', () => {
30-
render(<Node {...DEFAULT_PROPS} type="collection" data={{ title: 'employees', fields: [] }} />);
52+
render(
53+
<Node
54+
{...DEFAULT_PROPS}
55+
type="collection"
56+
data={{ title: 'employees', fields: [{ name: 'employeeId', type: 'string' }] }}
57+
/>,
58+
);
59+
expect(screen.getByRole('img', { name: 'Drag Icon' })).toBeInTheDocument();
60+
expect(screen.getByText('employees')).toBeInTheDocument();
61+
expect(screen.getByText('employeeId')).toBeInTheDocument();
62+
expect(screen.getByText('string')).toBeInTheDocument();
63+
});
64+
65+
it('Should show contextual zoom', () => {
66+
const mockedViewport = vi.mocked(useViewport);
67+
mockedViewport.mockReturnValue({ zoom: 0.2, x: 0, y: 0 });
68+
render(
69+
<Node
70+
{...DEFAULT_PROPS}
71+
type="collection"
72+
data={{ title: 'employees', fields: [{ name: 'employeeId', type: 'string' }] }}
73+
/>,
74+
);
75+
expect(screen.queryByRole('img', { name: 'Drag Icon' })).not.toBeInTheDocument();
3176
expect(screen.getByText('employees')).toBeInTheDocument();
77+
expect(screen.queryByText('employeeId')).not.toBeInTheDocument();
78+
expect(screen.queryByText('string')).not.toBeInTheDocument();
3279
});
3380
});

src/components/node/node.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
import { NodeProps } from '@xyflow/react';
1+
import { NodeProps, useViewport } from '@xyflow/react';
22
import styled from '@emotion/styled';
33
import { fontFamilies, spacing } from '@leafygreen-ui/tokens';
44
import { useTheme } from '@emotion/react';
55
import Icon from '@leafygreen-ui/icon';
66

77
import { ellipsisTruncation } from '@/styles/styles';
8-
import { DEFAULT_NODE_HEADER_HEIGHT, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
8+
import {
9+
DEFAULT_FIELD_HEIGHT,
10+
DEFAULT_FIELD_PADDING,
11+
DEFAULT_NODE_HEADER_HEIGHT,
12+
DEFAULT_NODE_WIDTH,
13+
ZOOM_THRESHOLD,
14+
} from '@/utilities/constants';
915
import { InternalNode } from '@/types/internal';
1016
import { NodeBorder } from '@/components/node/node-border';
1117
import { FieldList } from '@/components/field/field-list';
1218

19+
const NodeZoomedOut = styled.div<{ height: number }>`
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
height: ${props => props.height}px;
24+
`;
25+
26+
const NodeZoomedOutInner = styled.div`
27+
font-size: 20px;
28+
text-align: center;
29+
${ellipsisTruncation}
30+
`;
31+
1332
const NodeWrapper = styled.div<{ accent: string }>`
1433
position: relative;
1534
font-family: ${fontFamilies.code};
@@ -59,6 +78,7 @@ const NodeHeaderTitle = styled.div`
5978

6079
export const Node = ({ type, data: { title, fields, borderVariant } }: NodeProps<InternalNode>) => {
6180
const theme = useTheme();
81+
const { zoom } = useViewport();
6282

6383
const getAccent = () => {
6484
if (type === 'table') {
@@ -67,16 +87,27 @@ export const Node = ({ type, data: { title, fields, borderVariant } }: NodeProps
6787
return theme.node.mongoDBAccent;
6888
};
6989

90+
const isContextualZoom = zoom < ZOOM_THRESHOLD;
91+
7092
return (
7193
<NodeBorder variant={borderVariant}>
7294
<NodeWrapper accent={getAccent()}>
7395
<NodeHeader>
74-
<NodeHeaderIcon>
75-
<Icon fill={theme.node.headerIcon} glyph="Drag" />
76-
</NodeHeaderIcon>
77-
<NodeHeaderTitle>{title}</NodeHeaderTitle>
96+
{!isContextualZoom && (
97+
<>
98+
<NodeHeaderIcon>
99+
<Icon fill={theme.node.headerIcon} glyph="Drag" />
100+
</NodeHeaderIcon>
101+
<NodeHeaderTitle>{title}</NodeHeaderTitle>
102+
</>
103+
)}
78104
</NodeHeader>
79-
<FieldList accent={getAccent()} fields={fields} />
105+
{isContextualZoom && (
106+
<NodeZoomedOut height={fields.length * DEFAULT_FIELD_HEIGHT + DEFAULT_FIELD_PADDING * 2}>
107+
<NodeZoomedOutInner title={title}>{title}</NodeZoomedOutInner>
108+
</NodeZoomedOut>
109+
)}
110+
{!isContextualZoom && <FieldList accent={getAccent()} fields={fields} />}
80111
</NodeWrapper>
81112
</NodeBorder>
82113
);

src/utilities/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
export const ZOOM_THRESHOLD = 0.5;
12
export const DEFAULT_NODE_WIDTH = 244;
23
export const DEFAULT_NODE_HEIGHT = 80;
34
export const DEFAULT_NODE_SPACING = 100;
45
export const DEFAULT_NODE_STAR_SPACING = 50;
56
export const DEFAULT_NODE_HEADER_HEIGHT = 28;
67
export const DEFAULT_FIELD_HEIGHT = 18;
8+
export const DEFAULT_FIELD_PADDING = 8;
79
export const DEFAULT_DEPTH_SPACING = 8;

tsconfig.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@
1818
"./src/*"
1919
]
2020
},
21-
"types": ["node","vitest", "@testing-library/jest-dom", "@emotion/react"],
21+
"types": ["node","vitest", "@testing-library/jest-dom", "@emotion/react", "vitest/globals"],
2222
"typeRoots": ["./node_modules/@types", "./types", "./node_modules"],
2323
"sourceMap": true,
2424
"esModuleInterop": true,
2525
"noEmit": true,
2626
"strict": true,
2727
"noImplicitAny": true,
28-
"strictNullChecks": true,
28+
"strictNullChecks": true
2929

3030
},
3131
"include": [
32-
"src"
32+
"src",
3333
],
3434
"exclude": [
3535
"node_modules",
36-
"src/**/*.d.ts"
36+
"src/**/*.d.ts",
37+
".storybook"
3738
]
3839
}

0 commit comments

Comments
 (0)