Skip to content

Commit 1e52f7d

Browse files
Merge pull request #9 from CarstenWickner/improve-selection-handling
2 parents 351ae11 + 58182a5 commit 1e52f7d

8 files changed

Lines changed: 619 additions & 559 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@types/prop-types": "^15.7.2",
4343
"@types/react": "^16.9.20",
4444
"@types/react-dom": "^16.9.5",
45+
"@types/react-outside-click-handler": "^1.3.0",
4546
"@types/uuid": "^3.4.7",
4647
"@typescript-eslint/eslint-plugin": "^2.20.0",
4748
"@typescript-eslint/parser": "^2.20.0",
@@ -79,11 +80,12 @@
7980
"stylelint-config-recommended-scss": "^4.2.0",
8081
"stylelint-scss": "^3.13.0",
8182
"ts-jest": "^24.2.0",
82-
"typescript": "^3.6.5"
83+
"typescript": "~3.6.2"
8384
},
8485
"dependencies": {
8586
"react-dnd": "~10.0.2",
8687
"react-dnd-html5-backend": "~10.0.2",
88+
"react-outside-click-handler": "~1.3.0",
8789
"uuid": "~3.4.0"
8890
},
8991
"peerDependencies": {

src/component/FlowModeler.scss

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
}
127127
}
128128
}
129+
& .gateway-element.diverging + .menu {
130+
top: ($strokeStrength * 3) + (1.1em / 2);
131+
}
129132
& .stroke-horizontal::before,
130133
& .stroke-vertical::before {
131134
background-color: $strokeColor;
@@ -171,7 +174,7 @@
171174
z-index: -1;
172175
}
173176
&.can-drop::before {
174-
background-color: $dropStrokeColor;
177+
background: linear-gradient(90deg, $strokeColor 0%, $dropStrokeColor 62%);
175178
}
176179
& > .top-label {
177180
display: flex;
@@ -183,7 +186,14 @@
183186
min-height: $strokeStrength * 4;
184187
}
185188
& > .bottom-spacing {
186-
margin-top: $strokeStrength / 2;
189+
display: flex;
190+
flex-direction: column;
191+
margin-bottom: $strokeStrength / 2;
192+
}
193+
& .clickable-spacing {
194+
min-height: $strokeStrength * 4;
195+
max-height: $strokeStrength * 8;
196+
width: 100%;
187197
}
188198
}
189199
& .stroke-vertical {
@@ -212,12 +222,14 @@
212222
}
213223
&.editable {
214224
& .flow-element,
215-
& .top-label {
225+
& .top-label,
226+
& .clickable-spacing {
216227
cursor: context-menu;
217228
}
218229
& .flow-element.end-element,
219230
& .selected.flow-element,
220-
& .selected > .top-label {
231+
& .selected .top-label,
232+
& .selected .clickable-spacing {
221233
cursor: inherit;
222234
}
223235
& .start-element.selected,

src/component/FlowModeler.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as PropTypes from "prop-types";
22
import * as React from "react";
3+
import OutsideClickHandler from "react-outside-click-handler";
34
import { DndProvider } from "react-dnd";
45
import Backend from "react-dnd-html5-backend";
56

@@ -96,6 +97,12 @@ export class FlowModeler extends React.Component<FlowModelerProps, FlowModelerSt
9697
event.stopPropagation();
9798
};
9899

100+
handleOnOutsideClick = (): void => {
101+
if (this.state.selection !== null) {
102+
this.onSelect(null);
103+
}
104+
};
105+
99106
handleOnChange = (change: (originalFlow: FlowModelerProps["flow"]) => EditActionResult): void => {
100107
const { flow, onChange } = this.props;
101108
const { changedFlow } = change(flow);
@@ -216,22 +223,32 @@ export class FlowModeler extends React.Component<FlowModelerProps, FlowModelerSt
216223
};
217224
};
218225

219-
render(): React.ReactElement {
226+
renderMain(): React.ReactElement {
220227
const { flow, options, onChange } = this.props;
221228
const { gridCellData, columnCount } = buildRenderData(flow, options && options.verticalAlign === "bottom" ? "bottom" : "top");
222229
return (
223-
<DndProvider backend={Backend}>
224-
<div
225-
className={`flow-modeler${onChange ? " editable" : ""}`}
226-
onClick={onChange ? this.clearSelection : undefined}
227-
style={{ gridTemplateColumns: `repeat(${columnCount}, max-content)` }}
228-
>
229-
{gridCellData.map(this.renderGridCell(!!onChange))}
230-
</div>
231-
</DndProvider>
230+
<div
231+
className={`flow-modeler${onChange ? " editable" : ""}`}
232+
onClick={onChange ? this.clearSelection : undefined}
233+
style={{ gridTemplateColumns: `repeat(${columnCount}, max-content)` }}
234+
>
235+
{gridCellData.map(this.renderGridCell(!!onChange))}
236+
</div>
232237
);
233238
}
234239

240+
render(): React.ReactElement {
241+
const { onChange, options } = this.props;
242+
if (onChange) {
243+
return (
244+
<OutsideClickHandler onOutsideClick={this.handleOnOutsideClick}>
245+
{options && options.omitDndProvider ? this.renderMain() : <DndProvider backend={Backend}>{this.renderMain()}</DndProvider>}
246+
</OutsideClickHandler>
247+
);
248+
}
249+
return this.renderMain();
250+
}
251+
235252
static propTypes = {
236253
flow: PropTypes.shape({
237254
firstElementId: PropTypes.string,
@@ -254,7 +271,8 @@ export class FlowModeler extends React.Component<FlowModelerProps, FlowModelerSt
254271
).isRequired
255272
}).isRequired,
256273
options: PropTypes.shape({
257-
verticalAlign: PropTypes.oneOf(["top", "middle", "bottom"])
274+
verticalAlign: PropTypes.oneOf(["top", "middle", "bottom"]),
275+
omitDndProvider: PropTypes.bool
258276
}),
259277
renderStep: PropTypes.func.isRequired,
260278
renderGatewayConditionType: PropTypes.func,

src/component/HorizontalStroke.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export class HorizontalStroke extends React.Component<
6464
<div
6565
className="bottom-spacing"
6666
style={(children && this.topLabelRef.current && { minHeight: `${this.state.wrapperTopHeight}px` }) || undefined}
67-
/>
67+
>
68+
<div className="clickable-spacing" onClick={this.onTopLabelClick} />
69+
</div>
6870
</>
6971
)}
7072
</div>

src/types/FlowModelerProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface FlowModelerProps {
2020
};
2121
options?: {
2222
verticalAlign?: "top" | "middle" | "bottom";
23+
omitDndProvider?: boolean;
2324
};
2425
renderStep: (target: StepNode) => React.ReactNode;
2526
renderGatewayConditionType?: (target: DivergingGatewayNode) => React.ReactNode;

test/component/FlowModeler.test.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { shallow } from "enzyme";
2+
import { mount, shallow } from "enzyme";
33

44
import { FlowModeler } from "../../src/component/FlowModeler";
55

@@ -35,4 +35,32 @@ describe("renders correctly", () => {
3535
);
3636
expect(component).toMatchSnapshot();
3737
});
38+
it.each`
39+
elementType | cssSelector
40+
${"start node"} | ${".start-element"}
41+
${"step node"} | ${".step-element"}
42+
${"diverging gateway"} | ${".gateway-element.diverging"}
43+
${"converging gateway"} | ${".gateway-element.converging"}
44+
`("when selecting $elementType", ({ cssSelector }) => {
45+
const component = mount(
46+
<FlowModeler
47+
flow={simpleFlow}
48+
renderStep={({ data }): React.ReactNode => <>{data.label}</>}
49+
renderGatewayConditionType={({ data }): React.ReactNode => <>{data.label}</>}
50+
renderGatewayConditionValue={({ data }): React.ReactNode => <>{data.label}</>}
51+
onChange={(): void => {}}
52+
/>
53+
);
54+
let targetNode = component.find(cssSelector).at(0);
55+
expect(targetNode.hasClass("selected")).toBe(false);
56+
expect(component.find(".menu").exists()).toBe(false);
57+
58+
const onClick = targetNode.prop("onClick") as (event: React.MouseEvent) => void;
59+
onClick({ stopPropagation: (): void => {} } as React.MouseEvent);
60+
component.update();
61+
62+
targetNode = component.find(cssSelector).at(0);
63+
expect(targetNode.hasClass("selected")).toBe(true);
64+
expect(component.find(".menu").exists()).toBe(true);
65+
});
3866
});

0 commit comments

Comments
 (0)