Skip to content

Commit 3b2e662

Browse files
lazToumbkrem
authored andcommitted
Link Actions (click,mouseover,mouseout) (#230)
* Link Actions (click, mouseover, mouseout) * Update README.md * Coverage * More Tests * Update style.css
1 parent 1306492 commit 3b2e662

File tree

12 files changed

+560
-44
lines changed

12 files changed

+560
-44
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ class MyComponent extends React.Component {
9797
| `onClick` | `func` | | | `undefined` | Callback function to be called when a node is clicked. <br /><br />Has the function signature `(nodeData, evt)`. The clicked node's data object is passed as first parameter, event object as second. |
9898
| `onMouseOver` | `func` | | | `undefined` | Callback function to be called when mouse enters the space belonging to a node. <br /><br />Has the function signature `(nodeData, evt)`. The clicked node's data object is passed as first parameter, event object as second. |
9999
| `onMouseOut` | `func` | | | `undefined` | Callback function to be called when mouse leaves the space belonging to a node. <br /><br />Has the function signature `(nodeData, evt)`. The clicked node's data object is passed as first parameter, event object as second. |
100+
| `onLinkClick` | `func` | | | `undefined` | Callback function to be called when a link is clicked. <br /><br />Has the function signature `(linkSource, linkTarget, evt)`. The clicked link's parent data object is passed as first parameter, the child's as second, the event object as third. |
101+
| `onLinkMouseOver` | `func` | | | `undefined` | Callback function to be called when mouse enters the space belonging to a link. <br /><br />Has the function signature `(linkSource, linkTarget, evt)`. The clicked link's parent data object is passed as first parameter, the child's as second, the event object as third. |
102+
| `onLinkMouseOut` | `func` | | | `undefined` | Callback function to be called when mouse leaves the space belonging to a link. <br /><br />Has the function signature `(linkSource, linkTarget, evt)`. The clicked link's parent data object is passed as first parameter, the child's as second, the event object as third. |
100103
| `onUpdate` | `func` | | | `undefined` | Callback function to be called when the inner D3 component updates. That is - on every zoom or translate event, or when tree branches are toggled. The node's data object, as well as zoom level and coordinates are passed to the callback. |
101104
| `orientation` | `string` (enum) | `horizontal` `vertical` | | `horizontal` | `horizontal` - Tree expands left-to-right. <br /><br /> `vertical` - Tree expands top-to-bottom. |
102105
| `translate` | `object` | | | `{x: 0, y: 0}` | Translates the graph along the x/y axis by the specified amount of pixels (avoids the graph being stuck in the top left canvas corner). |
@@ -143,10 +146,10 @@ const svgSquare = {
143146
To avoid rendering any node element, simply set `nodeSvgShape` to `{ shape: 'none' }`.
144147

145148
### Overridable `shapeProps`
146-
`shapeProps` is currently merged with `node.circle`/`leafNode.circle` (see [Styling](#styling)).
149+
`shapeProps` is currently merged with `node.circle`/`leafNode.circle` (see [Styling](#styling)).
147150

148-
This means any properties passed in `shapeProps` will be overridden by **properties with the same key** in the `node.circle`/`leafNode.circle` style props.
149-
This is to prevent breaking the legacy usage of `circleRadius` + styling via `node/leafNode` properties until it is deprecated fully in v2.
151+
This means any properties passed in `shapeProps` will be overridden by **properties with the same key** in the `node.circle`/`leafNode.circle` style props.
152+
This is to prevent breaking the legacy usage of `circleRadius` + styling via `node/leafNode` properties until it is deprecated fully in v2.
150153

151154
**From v1.5.x onwards, it is therefore recommended to pass all node styling properties through `shapeProps`**.
152155

docs/components/Link.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ Props
1111
type: `object`
1212

1313

14+
### `onClick` (required)
15+
16+
type: `func`
17+
18+
19+
### `onMouseOut` (required)
20+
21+
type: `func`
22+
23+
24+
### `onMouseOver` (required)
25+
26+
type: `func`
27+
28+
1429
### `orientation` (required)
1530

1631
type: `enum('horizontal'|'vertical')`

docs/components/Tree.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ type: `func`
7070
defaultValue: `undefined`
7171

7272

73+
### `onLinkClick`
74+
75+
type: `func`
76+
defaultValue: `undefined`
77+
78+
79+
### `onLinkMouseOut`
80+
81+
type: `func`
82+
defaultValue: `undefined`
83+
84+
85+
### `onLinkMouseOver`
86+
87+
type: `func`
88+
defaultValue: `undefined`
89+
90+
7391
### `onMouseOut`
7492

7593
type: `func`

lib/react-d3-tree.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Link/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ export default class Link extends React.PureComponent {
8585
return this.drawDiagonalPath(linkData, orientation);
8686
}
8787

88+
handleOnClick = evt => {
89+
this.props.onClick(this.props.linkData.source, this.props.linkData.target, evt);
90+
};
91+
92+
handleOnMouseOver = evt => {
93+
this.props.onMouseOver(this.props.linkData.source, this.props.linkData.target, evt);
94+
};
95+
96+
handleOnMouseOut = evt => {
97+
this.props.onMouseOut(this.props.linkData.source, this.props.linkData.target, evt);
98+
};
99+
88100
render() {
89101
const { styles } = this.props;
90102
return (
@@ -95,6 +107,9 @@ export default class Link extends React.PureComponent {
95107
style={{ ...this.state.initialStyle, ...styles }}
96108
className="linkBase"
97109
d={this.drawPath()}
110+
onClick={this.handleOnClick}
111+
onMouseOver={this.handleOnMouseOver}
112+
onMouseOut={this.handleOnMouseOut}
98113
/>
99114
);
100115
}
@@ -109,5 +124,8 @@ Link.propTypes = {
109124
orientation: T.oneOf(['horizontal', 'vertical']).isRequired,
110125
pathFunc: T.oneOfType([T.oneOf(['diagonal', 'elbow', 'straight']), T.func]).isRequired,
111126
transitionDuration: T.number.isRequired,
127+
onClick: T.func.isRequired,
128+
onMouseOver: T.func.isRequired,
129+
onMouseOut: T.func.isRequired,
112130
styles: T.object,
113131
};

src/Link/tests/index.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ describe('<Link />', () => {
2121
pathFunc: 'diagonal',
2222
orientation: 'horizontal',
2323
transitionDuration: 500,
24+
onClick: () => {},
25+
onMouseOver: () => {},
26+
onMouseOut: () => {},
2427
styles: {},
2528
};
2629

@@ -75,6 +78,20 @@ describe('<Link />', () => {
7578
);
7679
});
7780

81+
it('returns an appropriate diagonal according to `props.orientation`', () => {
82+
const ymean = (linkData.target.y + linkData.source.y) / 2;
83+
expect(Link.prototype.drawDiagonalPath(linkData, 'horizontal')).toBe(
84+
`M${linkData.source.y},${linkData.source.x}` +
85+
`C${ymean},${linkData.source.x} ${ymean},${linkData.target.x} ` +
86+
`${linkData.target.y},${linkData.target.x}`,
87+
);
88+
expect(Link.prototype.drawDiagonalPath(linkData, 'vertical')).toBe(
89+
`M${linkData.source.x},${linkData.source.y}` +
90+
`C${linkData.source.x},${ymean} ${linkData.target.x},${ymean} ` +
91+
`${linkData.target.x},${linkData.target.y}`,
92+
);
93+
});
94+
7895
it('returns an appropriate straightPath according to `props.orientation`', () => {
7996
expect(Link.prototype.drawStraightPath(linkData, 'horizontal')).toBe(
8097
`M${linkData.source.y},${linkData.source.x}L${linkData.target.y},${linkData.target.x}`,
@@ -93,4 +110,48 @@ describe('<Link />', () => {
93110
mockProps.transitionDuration,
94111
);
95112
});
113+
114+
describe('Events', () => {
115+
it('handles onClick events and passes its nodeId & event object to onClick handler', () => {
116+
const onClickSpy = jest.fn();
117+
const mockEvt = { mock: 'event' };
118+
const renderedComponent = shallow(<Link {...mockProps} onClick={onClickSpy} />);
119+
120+
renderedComponent.simulate('click', mockEvt);
121+
expect(onClickSpy).toHaveBeenCalledTimes(1);
122+
expect(onClickSpy).toHaveBeenCalledWith(
123+
linkData.source,
124+
linkData.target,
125+
expect.objectContaining(mockEvt),
126+
);
127+
});
128+
129+
it('handles onMouseOver events and passes its nodeId & event object to onMouseOver handler', () => {
130+
const onMouseOverSpy = jest.fn();
131+
const mockEvt = { mock: 'event' };
132+
const renderedComponent = shallow(<Link {...mockProps} onMouseOver={onMouseOverSpy} />);
133+
134+
renderedComponent.simulate('mouseover', mockEvt);
135+
expect(onMouseOverSpy).toHaveBeenCalledTimes(1);
136+
expect(onMouseOverSpy).toHaveBeenCalledWith(
137+
linkData.source,
138+
linkData.target,
139+
expect.objectContaining(mockEvt),
140+
);
141+
});
142+
143+
it('handles onMouseOut events and passes its nodeId & event object to onMouseOut handler', () => {
144+
const onMouseOutSpy = jest.fn();
145+
const mockEvt = { mock: 'event' };
146+
const renderedComponent = shallow(<Link {...mockProps} onMouseOut={onMouseOutSpy} />);
147+
148+
renderedComponent.simulate('mouseout', mockEvt);
149+
expect(onMouseOutSpy).toHaveBeenCalledTimes(1);
150+
expect(onMouseOutSpy).toHaveBeenCalledWith(
151+
linkData.source,
152+
linkData.target,
153+
expect.objectContaining(mockEvt),
154+
);
155+
});
156+
});
96157
});

src/Node/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export default class Node extends React.Component {
2121
this.applyTransform(transform, transitionDuration);
2222
}
2323

24-
componentWillUpdate(nextProps) {
24+
// eslint-disable-next-line camelcase
25+
UNSAFE_componentWillUpdate(nextProps) {
2526
const transform = this.setTransform(nextProps.nodeData, nextProps.orientation);
2627
this.applyTransform(transform, nextProps.transitionDuration);
2728
}
@@ -39,8 +40,9 @@ export default class Node extends React.Component {
3940
setTransform(nodeData, orientation, shouldTranslateToOrigin = false) {
4041
const { x, y, parent } = nodeData;
4142
if (shouldTranslateToOrigin) {
42-
const originX = parent ? parent.x : 0;
43-
const originY = parent ? parent.y : 0;
43+
const hasParent = typeof parent === 'object';
44+
const originX = hasParent ? parent.x : 0;
45+
const originY = hasParent ? parent.y : 0;
4446
return orientation === 'horizontal'
4547
? `translate(${originY},${originX})`
4648
: `translate(${originX},${originY})`;
@@ -79,7 +81,7 @@ export default class Node extends React.Component {
7981
});
8082
};
8183

82-
handleClick = evt => {
84+
handleOnClick = evt => {
8385
this.props.onClick(this.props.nodeData.id, evt);
8486
};
8587

@@ -109,7 +111,7 @@ export default class Node extends React.Component {
109111
style={this.state.initialStyle}
110112
className={nodeData._children ? 'nodeBase' : 'leafNodeBase'}
111113
transform={this.state.transform}
112-
onClick={this.handleClick}
114+
onClick={this.handleOnClick}
113115
onMouseOver={this.handleOnMouseOver}
114116
onMouseOut={this.handleOnMouseOut}
115117
>

src/Node/tests/index.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ describe('<Node />', () => {
166166
});
167167

168168
it('updates its position if `orientation` changes', () => {
169-
const renderedComponent = mount(<Node {...mockProps} />);
170-
const nextProps = { ...mockProps, orientation: 'vertical' };
171-
169+
const thisProps = { ...mockProps, shouldTranslateToOrigin: true, orientation: 'horizontal' };
170+
delete thisProps.parent;
171+
const renderedComponent = mount(<Node {...thisProps} />);
172+
const nextProps = { ...thisProps, orientation: 'vertical' };
172173
expect(
173174
renderedComponent.instance().shouldNodeTransform(renderedComponent.props(), nextProps),
174175
).toBe(true);

src/Tree/NodeWrapper.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export default class NodeWrapper extends React.Component {
77
enableTransitions: this.props.transitionDuration > 0,
88
};
99

10-
componentWillReceiveProps(nextProps) {
10+
// eslint-disable-next-line camelcase
11+
UNSAFE_componentWillReceiveProps(nextProps) {
1112
if (nextProps.transitionDuration !== this.props.transitionDuration) {
1213
this.setState({
1314
enableTransitions: nextProps.transitionDuration > 0,

0 commit comments

Comments
 (0)