Skip to content

Commit d33fe5e

Browse files
authored
feat/COMPASS-9504 add expand collapse toggle to the node title (#152)
* feat/COMPASS-9504 add expand collapse toggle to the node title * Pass nodeId in the callback * add storybook story
1 parent 3b9d5d5 commit d33fe5e

File tree

8 files changed

+126
-12
lines changed

8 files changed

+126
-12
lines changed

src/components/buttons/diagram-icon-button.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const StyledDiagramIconButton = styled.button`
99
outline: none;
1010
padding: ${spacing[100]}px;
1111
margin: 0;
12-
margin-left: ${spacing[100]}px;
1312
cursor: pointer;
1413
color: inherit;
1514
display: flex;

src/components/canvas/canvas.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const Canvas = ({
5858
onConnect,
5959
id,
6060
onAddFieldToNodeClick,
61+
onNodeExpandToggle,
6162
onAddFieldToObjectFieldClick,
6263
onFieldNameChange,
6364
onFieldClick,
@@ -147,6 +148,7 @@ export const Canvas = ({
147148
<EditableDiagramInteractionsProvider
148149
onFieldClick={onFieldClick}
149150
onAddFieldToNodeClick={onAddFieldToNodeClick}
151+
onNodeExpandToggle={onNodeExpandToggle}
150152
onAddFieldToObjectFieldClick={onAddFieldToObjectFieldClick}
151153
onFieldNameChange={onFieldNameChange}
152154
>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useTheme } from '@emotion/react';
2+
3+
export const ChevronCollapse = ({ size = 14 }: { size?: number }) => {
4+
const theme = useTheme();
5+
return (
6+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
7+
<path
8+
d="M4.25 3.25L8 6.5L11.75 3.25M4.25 12.75L8 9.5L11.75 12.75"
9+
stroke={theme.node.headerIcon}
10+
strokeWidth="1.5"
11+
strokeLinecap="round"
12+
strokeLinejoin="round"
13+
/>
14+
</svg>
15+
);
16+
};

src/components/node/node.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ import { EditableDiagramInteractionsProvider } from '@/hooks/use-editable-diagra
1010

1111
const Node = ({
1212
onAddFieldToNodeClick,
13+
onNodeExpandToggle,
1314
...props
1415
}: React.ComponentProps<typeof NodeComponent> & {
1516
onAddFieldToNodeClick?: () => void;
17+
onNodeExpandToggle?: () => void;
1618
}) => (
17-
<EditableDiagramInteractionsProvider onAddFieldToNodeClick={onAddFieldToNodeClick}>
19+
<EditableDiagramInteractionsProvider
20+
onAddFieldToNodeClick={onAddFieldToNodeClick}
21+
onNodeExpandToggle={onNodeExpandToggle}
22+
>
1823
<NodeComponent {...props} />
1924
</EditableDiagramInteractionsProvider>
2025
);
@@ -106,6 +111,18 @@ describe('node', () => {
106111
expect(onAddFieldToNodeClickMock).toHaveBeenCalled();
107112
});
108113

114+
it('Should show a clickable button to toggle expand collapse when onNodeExpandToggle is supplied', async () => {
115+
const onNodeExpandToggleMock = vi.fn();
116+
117+
render(<Node {...DEFAULT_PROPS} onNodeExpandToggle={onNodeExpandToggleMock} />);
118+
const button = screen.getByRole('button', { name: 'Toggle Expand / Collapse Fields' });
119+
expect(button).toBeInTheDocument();
120+
expect(button).toHaveAttribute('title', 'Toggle Expand / Collapse Fields');
121+
expect(onNodeExpandToggleMock).not.toHaveBeenCalled();
122+
await userEvent.click(button);
123+
expect(onNodeExpandToggleMock).toHaveBeenCalled();
124+
});
125+
109126
it('Should prioritise borderVariant over selected prop when setting the border', () => {
110127
render(
111128
<Node {...DEFAULT_PROPS} selected type="collection" data={{ ...DEFAULT_PROPS.data, borderVariant: 'subtle' }} />,

src/components/node/node.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
88
import { DEFAULT_NODE_HEADER_HEIGHT, ZOOM_THRESHOLD } from '@/utilities/constants';
99
import { InternalNode } from '@/types/internal';
1010
import { PlusWithSquare } from '@/components/icons/plus-with-square';
11+
import { ChevronCollapse } from '@/components/icons/chevron-collapse';
1112
import { NodeBorder } from '@/components/node/node-border';
1213
import { FieldList } from '@/components/field/field-list';
1314
import { NodeType } from '@/types';
@@ -102,9 +103,14 @@ const NodeWithFields = styled.div<{ visibility: string }>`
102103
visibility: ${props => props.visibility};
103104
`;
104105

105-
const AddNewFieldIconButtonButton = styled(DiagramIconButton)`
106+
const TitleControlsContainer = styled.div`
106107
margin-left: auto;
107108
margin-right: ${spacing[200]}px;
109+
display: flex;
110+
gap: ${spacing[50]}px;
111+
& > * {
112+
flex: 0 0 auto;
113+
}
108114
`;
109115

110116
export const Node = ({
@@ -119,7 +125,7 @@ export const Node = ({
119125

120126
const [isHovering, setHovering] = useState(false);
121127

122-
const { onClickAddFieldToNode: addFieldToNodeClickHandler } = useEditableDiagramInteractions();
128+
const { onClickAddFieldToNode: addFieldToNodeClickHandler, onNodeExpandToggle } = useEditableDiagramInteractions();
123129

124130
const onClickAddFieldToNode = useCallback(
125131
(event: React.MouseEvent<HTMLButtonElement>) => {
@@ -129,6 +135,13 @@ export const Node = ({
129135
[addFieldToNodeClickHandler, id],
130136
);
131137

138+
const handleNodeExpandToggle = useCallback(
139+
(event: React.MouseEvent<HTMLButtonElement>) => {
140+
onNodeExpandToggle?.(event, id);
141+
},
142+
[onNodeExpandToggle, id],
143+
);
144+
132145
const getAccent = () => {
133146
if (disabled && !isHovering) {
134147
return theme.node.disabledAccent;
@@ -210,11 +223,22 @@ export const Node = ({
210223
<Icon fill={theme.node.headerIcon} glyph="Drag" />
211224
</NodeHeaderIcon>
212225
<NodeHeaderTitle>{title}</NodeHeaderTitle>
213-
{addFieldToNodeClickHandler && (
214-
<AddNewFieldIconButtonButton aria-label="Add Field" onClick={onClickAddFieldToNode} title="Add Field">
215-
<PlusWithSquare />
216-
</AddNewFieldIconButtonButton>
217-
)}
226+
<TitleControlsContainer>
227+
{addFieldToNodeClickHandler && (
228+
<DiagramIconButton aria-label="Add Field" onClick={onClickAddFieldToNode} title="Add Field">
229+
<PlusWithSquare />
230+
</DiagramIconButton>
231+
)}
232+
{onNodeExpandToggle && (
233+
<DiagramIconButton
234+
aria-label="Toggle Expand / Collapse Fields"
235+
onClick={handleNodeExpandToggle}
236+
title="Toggle Expand / Collapse Fields"
237+
>
238+
<ChevronCollapse />
239+
</DiagramIconButton>
240+
)}
241+
</TitleControlsContainer>
218242
</NodeHeader>
219243
<FieldList nodeId={id} nodeType={type as NodeType} isHovering={isHovering} fields={fields} />
220244
</NodeWithFields>

src/hooks/use-editable-diagram-interactions.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import React, { createContext, useContext, useMemo, ReactNode } from 'react';
33
import {
44
OnFieldClickHandler,
55
OnAddFieldToNodeClickHandler,
6+
OnNodeExpandHandler,
67
OnAddFieldToObjectFieldClickHandler,
78
OnFieldNameChangeHandler,
89
} from '@/types';
910

1011
interface EditableDiagramInteractionsContextType {
1112
onClickField?: OnFieldClickHandler;
1213
onClickAddFieldToNode?: OnAddFieldToNodeClickHandler;
14+
onNodeExpandToggle?: OnNodeExpandHandler;
1315
onClickAddFieldToObjectField?: OnAddFieldToObjectFieldClickHandler;
1416
onChangeFieldName?: OnFieldNameChangeHandler;
1517
}
@@ -20,6 +22,7 @@ interface EditableDiagramInteractionsProviderProps {
2022
children: ReactNode;
2123
onFieldClick?: OnFieldClickHandler;
2224
onAddFieldToNodeClick?: OnAddFieldToNodeClickHandler;
25+
onNodeExpandToggle?: OnNodeExpandHandler;
2326
onAddFieldToObjectFieldClick?: OnAddFieldToObjectFieldClickHandler;
2427
onFieldNameChange?: OnFieldNameChangeHandler;
2528
}
@@ -28,6 +31,7 @@ export const EditableDiagramInteractionsProvider: React.FC<EditableDiagramIntera
2831
children,
2932
onFieldClick,
3033
onAddFieldToNodeClick,
34+
onNodeExpandToggle,
3135
onAddFieldToObjectFieldClick,
3236
onFieldNameChange,
3337
}) => {
@@ -43,6 +47,11 @@ export const EditableDiagramInteractionsProvider: React.FC<EditableDiagramIntera
4347
onClickAddFieldToNode: onAddFieldToNodeClick,
4448
}
4549
: undefined),
50+
...(onNodeExpandToggle
51+
? {
52+
onNodeExpandToggle: onNodeExpandToggle,
53+
}
54+
: undefined),
4655
...(onAddFieldToObjectFieldClick
4756
? {
4857
onClickAddFieldToObjectField: onAddFieldToObjectFieldClick,
@@ -54,7 +63,7 @@ export const EditableDiagramInteractionsProvider: React.FC<EditableDiagramIntera
5463
}
5564
: undefined),
5665
};
57-
}, [onFieldClick, onAddFieldToNodeClick, onAddFieldToObjectFieldClick, onFieldNameChange]);
66+
}, [onFieldClick, onAddFieldToNodeClick, onNodeExpandToggle, onAddFieldToObjectFieldClick, onFieldNameChange]);
5867

5968
return (
6069
<EditableDiagramInteractionsContext.Provider value={value}>{children}</EditableDiagramInteractionsContext.Provider>

src/mocks/decorators/diagram-editable-interactions.decorator.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef, useState, MouseEvent as ReactMouseEvent } from 'react';
1+
import { useCallback, useEffect, useRef, useState, MouseEvent as ReactMouseEvent, useMemo } from 'react';
22
import { Decorator } from '@storybook/react';
33

44
import { DiagramProps, FieldId, NodeField, NodeProps } from '@/types';
@@ -88,6 +88,13 @@ function editableNodesFromNodes(nodes: NodeProps[]): NodeProps[] {
8888

8989
export const useEditableNodes = (initialNodes: NodeProps[]) => {
9090
const [nodes, setNodes] = useState<NodeProps[]>([]);
91+
const [expanded, setExpanded] = useState<Record<string, boolean>>(() => {
92+
return Object.fromEntries(
93+
nodes.map(node => {
94+
return [node.id, true];
95+
}),
96+
);
97+
});
9198

9299
const hasInitialized = useRef(false);
93100
useEffect(() => {
@@ -185,7 +192,37 @@ export const useEditableNodes = (initialNodes: NodeProps[]) => {
185192
);
186193
}, []);
187194

188-
return { nodes, onFieldClick, onAddFieldToNodeClick, onAddFieldToObjectFieldClick, onFieldNameChange };
195+
const onNodeExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string) => {
196+
setExpanded(state => {
197+
return {
198+
...state,
199+
[nodeId]: !state[nodeId],
200+
};
201+
});
202+
}, []);
203+
204+
const _nodes = useMemo(() => {
205+
return nodes.map(node => {
206+
if (expanded[node.id]) {
207+
return node;
208+
}
209+
return {
210+
...node,
211+
fields: node.fields.filter(field => {
212+
return !field.depth || field.depth === 0;
213+
}),
214+
};
215+
});
216+
}, [nodes, expanded]);
217+
218+
return {
219+
nodes: _nodes,
220+
onFieldClick,
221+
onAddFieldToNodeClick,
222+
onNodeExpandToggle,
223+
onAddFieldToObjectFieldClick,
224+
onFieldNameChange,
225+
};
189226
};
190227

191228
export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (Story, context) => {

src/types/component-props.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export type OnFieldClickHandler = (
2525
*/
2626
export type OnAddFieldToNodeClickHandler = (event: ReactMouseEvent, nodeId: string) => void;
2727

28+
/**
29+
* Called when the button to expand / collapse all field is clicked in the node header.
30+
*/
31+
export type OnNodeExpandHandler = (event: ReactMouseEvent, nodeId: string) => void;
32+
2833
/**
2934
* Called when the button to add a new field is clicked on an object type field in a node.
3035
*/
@@ -184,6 +189,11 @@ export interface DiagramProps {
184189
*/
185190
onAddFieldToNodeClick?: OnAddFieldToNodeClickHandler;
186191

192+
/**
193+
* Callback when the user clicks the button to expand / collapse all fields in the node.
194+
*/
195+
onNodeExpandToggle?: OnNodeExpandHandler;
196+
187197
/**
188198
* Callback when the user clicks to add a new field to an object type field in a node.
189199
*/

0 commit comments

Comments
 (0)