Skip to content

Commit df7a0cc

Browse files
Merge pull request #28 from qiushihe/dom-validation
Add validation and update README.
2 parents f63f3d5 + a7515ab commit df7a0cc

5 files changed

Lines changed: 226 additions & 93 deletions

File tree

README.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,22 @@ To include an instance of FlowTip in your component:
4747
```javascript
4848
import flowtip from 'flowtip/lib/dom';
4949

50-
// Generate a FlowTip™ with the tail and content you want.
51-
const MyFlowTip = flowtip({
52-
tail: toClass(({style, region}) => (
53-
<div className={`flowtip-tail flowtip-tail-${region}`} style={style}/>
54-
)),
55-
content: toClass(({style, region, children}) => (
56-
<div className={`flowtip-root flowtip-root-${region}`} style={style}>
57-
{children}
58-
</div>
59-
)),
60-
});
61-
62-
// Use your new FlowTip™.
50+
// Define the FlowTip content component
51+
const ContentComponent = ({style, region, children}) => (
52+
<div className={`flowtip-root flowtip-root-${region}`} style={style}>
53+
{children}
54+
</div>
55+
);
56+
57+
// Define the FlowTip tail component
58+
const TailComponent = ({style, region}) => (
59+
<div className={`flowtip-tail flowtip-tail-${region}`} style={style} />
60+
);
61+
62+
// Generate a FlowTip™ component with the content and tail components.
63+
const MyFlowTip = flowtip(ContentComponent, TailComponent);
64+
65+
// Render the FlowTip™ component.
6366
const target = {
6467
top: 5,
6568
left: 5,
@@ -199,7 +202,7 @@ The tooltip will be squeezed to an adjacent region if the root of the tooltip ge
199202

200203
## Alignments
201204

202-
Tool tip's alignments are divided into **root alignment** and **target alignment**, each with a corresponding **offset** attribute that controls the direction of the alignment and offset amount.
205+
Tooltip's alignments are divided into **root alignment** and **target alignment**, each with a corresponding **offset** attribute that controls the direction of the alignment and offset amount.
203206

204207
### Target Alignment
205208

@@ -208,3 +211,11 @@ Target alignment refers to the alignment of the pivot relative to the target of
208211
### Root Alignment
209212

210213
Root alignment refers to the alignment of the tooltip's root relative to the pivot. See `rootAlign` and `rootAlignOffset`.
214+
215+
## Clamping
216+
217+
By default the tooltip is "clamped" to its parent. Meaning even if the target leaves the viewport, the tooltip would never leave the viewport. The clamping behaviour can be controlled via the `clamp` property.
218+
219+
When `clamp` is `true`, the flyout will always remains in the parent even if the target is out of the parent.
220+
221+
When `clamp` is `false`, the flyout will always attatch itself to the target, even if it’s outside the parent, but it will make a best effort to be in the parent.

demo/demo.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ class FlowTipDemo extends Component {
2929
this.updateTargetProperties();
3030
}
3131

32-
updateTargetProperties() {
33-
this.setState({
34-
target: ReactDOM.findDOMNode(this.refs.target).getBoundingClientRect(),
35-
});
36-
}
37-
3832
handleTargetMove() {
3933
this.updateTargetProperties();
4034
}
@@ -45,6 +39,12 @@ class FlowTipDemo extends Component {
4539
});
4640
}
4741

42+
updateTargetProperties() {
43+
this.setState({
44+
target: ReactDOM.findDOMNode(this.refs.target).getBoundingClientRect(),
45+
});
46+
}
47+
4848
render() {
4949
const flowtipProperties = {
5050
targetOffset: 10,
@@ -99,6 +99,16 @@ class FlowTipDemo extends Component {
9999
class FlowTipDemoTarget extends Component {
100100
state = {posX: 0, posY: 0, active: true};
101101

102+
componentDidMount() {
103+
window.addEventListener('mousemove', this.handleMouseMove.bind(this));
104+
window.addEventListener('click', this.handleMouseClick.bind(this));
105+
}
106+
107+
componentWillUnmount() {
108+
window.removeEventListener('mousemove', this.handleMouseMove.bind(this));
109+
window.removeEventListener('click', this.handleMouseClick.bind(this));
110+
}
111+
102112
handleMouseMove(ev) {
103113
if (!this.state.active) {
104114
return;
@@ -117,16 +127,6 @@ class FlowTipDemoTarget extends Component {
117127
this.setState({active: !this.state.active});
118128
}
119129

120-
componentDidMount() {
121-
window.addEventListener('mousemove', this.handleMouseMove.bind(this));
122-
window.addEventListener('click', this.handleMouseClick.bind(this));
123-
}
124-
125-
componentWillUnmount() {
126-
window.removeEventListener('mousemove', this.handleMouseMove.bind(this));
127-
window.removeEventListener('click', this.handleMouseClick.bind(this));
128-
}
129-
130130
render() {
131131
const style = {
132132
position: 'absolute',

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"react": "^15.3.1",
5353
"react-addons-test-utils": "^15.3.1",
5454
"react-dom": "^15.3.1",
55+
"sinon": "^1.17.6",
5556
"webpack": "^1.12.11",
5657
"webpack-dev-server": "^1.14.1"
5758
},

src/dom.js

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,20 @@
1-
/* global getComputedStyle window */
1+
/* global window */
22
import {createElement, Component} from 'react';
33
import ReactDOM from 'react-dom';
44

55
import FlowTip from './flowtip';
66
import ResizeObserver from 'react-resize-observer';
77

8-
/**
9-
* Find the closest node that will control the positioning of the FlowTip™
10-
* content.
11-
* @param {Node} _node Initial node to search from.
12-
* @returns {Node} The anchor parent.
13-
*/
14-
const getAnchorParent = (_node) => {
15-
let node = _node.parentNode;
16-
const isParentNode = (node) => {
17-
const style = getComputedStyle(node);
18-
return style && style.position !== 'static';
19-
};
20-
21-
while (node) {
22-
if (isParentNode(node) || node.tagName === 'BODY') {
23-
return node;
24-
}
25-
node = node.parentNode;
8+
export default (Content, Tail) => {
9+
if (typeof(Content) !== 'function') {
10+
throw new TypeError('Content component is not a function.');
2611
}
27-
return node;
28-
};
29-
30-
/**
31-
* Find the closest node that should enclose the FlowTip™'s content. Basically
32-
* the nearest thing with scrollbars.
33-
* @param {[type]} _node [description]
34-
* @param {[type]} parentClass = null [description]
35-
* @returns {[type]} [description]
36-
*/
37-
const getBoundingParent = (_node, parentClass = null) => {
38-
let node = _node.parentNode;
39-
const scrollishStyle = (style) => [
40-
'auto',
41-
'hidden',
42-
'scroll',
43-
].indexOf(style) !== -1;
44-
45-
const isParentNode = (node) => {
46-
const style = getComputedStyle(node);
47-
if (parentClass) {
48-
return node.className.indexOf(parentClass) !== -1;
49-
} else if (style) {
50-
return scrollishStyle(style.overflow) ||
51-
scrollishStyle(style.overflowX) ||
52-
scrollishStyle(style.overflowY);
53-
}
54-
return false;
55-
};
5612

57-
while (node) {
58-
if (isParentNode(node) || node.tagName === 'BODY') {
59-
return node;
60-
}
61-
node = node.parentNode;
13+
if (typeof(Tail) !== 'function') {
14+
throw new TypeError('Tail component is not a function.');
6215
}
63-
return node;
64-
};
6516

66-
export default (Content, Tail) => {
67-
return class MyFlowTip extends Component {
17+
return class FlowTipDOM extends Component {
6818
static defaultProps = {
6919
clamp: true,
7020
};
@@ -81,13 +31,75 @@ export default (Content, Tail) => {
8131
this.handleScroll = this.handleScroll.bind(this);
8232
}
8333

34+
getWindow() {
35+
return window;
36+
}
37+
38+
/**
39+
* Find the closest node that will control the positioning of the FlowTip™
40+
* content.
41+
* @param {Node} _node Initial node to search from.
42+
* @returns {Node} The anchor parent.
43+
*/
44+
getAnchorParent(_node) {
45+
let node = _node.parentNode;
46+
const isParentNode = (node) => {
47+
const style = this.getWindow().getComputedStyle(node);
48+
return style && style.position !== 'static';
49+
};
50+
51+
while (node) {
52+
if (isParentNode(node) || node.tagName === 'BODY') {
53+
return node;
54+
}
55+
node = node.parentNode;
56+
}
57+
return node;
58+
}
59+
60+
/**
61+
* Find the closest node that should enclose the FlowTip™'s content.
62+
* Basically the nearest thing with scrollbars.
63+
* @param {[type]} _node [description]
64+
* @param {[type]} parentClass = null [description]
65+
* @returns {[type]} [description]
66+
*/
67+
getBoundingParent(_node, parentClass = null) {
68+
let node = _node.parentNode;
69+
const scrollishStyle = (style) => [
70+
'auto',
71+
'hidden',
72+
'scroll',
73+
].indexOf(style) !== -1;
74+
75+
const isParentNode = (node) => {
76+
const style = this.getWindow().getComputedStyle(node);
77+
if (parentClass) {
78+
return node.className.indexOf(parentClass) !== -1;
79+
} else if (style) {
80+
return scrollishStyle(style.overflow) ||
81+
scrollishStyle(style.overflowX) ||
82+
scrollishStyle(style.overflowY);
83+
}
84+
return false;
85+
};
86+
87+
while (node) {
88+
if (isParentNode(node) || node.tagName === 'BODY') {
89+
return node;
90+
}
91+
node = node.parentNode;
92+
}
93+
return node;
94+
}
95+
8496
getAnchorElement() {
85-
return getAnchorParent(ReactDOM.findDOMNode(this.refs.flowtip));
97+
return this.getAnchorParent(ReactDOM.findDOMNode(this.refs.flowtip));
8698
}
8799

88100
getParentElement() {
89101
const {parentClass} = this.props;
90-
return getBoundingParent(this.getAnchorElement(), parentClass);
102+
return this.getBoundingParent(this.getAnchorElement(), parentClass);
91103
}
92104

93105
getAnchorRect() {
@@ -123,11 +135,11 @@ export default (Content, Tail) => {
123135
parent.top = Math.max(parent.top, 0);
124136
parent.height = Math.min(
125137
parent.height,
126-
window.innerHeight - parent.top
138+
this.getWindow().innerHeight - parent.top
127139
);
128140
parent.width = Math.min(
129141
parent.width,
130-
window.innerWidth - parent.left
142+
this.getWindow().innerWidth - parent.left
131143
);
132144
}
133145
parent.width -= scrollerWidth;
@@ -150,13 +162,13 @@ export default (Content, Tail) => {
150162
componentDidMount() {
151163
const parent = this.getParentElement();
152164
this.updateState();
153-
window.addEventListener('scroll', this.handleScroll);
165+
this.getWindow().addEventListener('scroll', this.handleScroll);
154166
parent.addEventListener('scroll', this.handleScroll);
155167
}
156168

157169
componentWillUnmount() {
158170
const parent = this.getParentElement();
159-
window.removeEventListener('scroll', this.handleScroll);
171+
this.getWindow().removeEventListener('scroll', this.handleScroll);
160172
parent.removeEventListener('scroll', this.handleScroll);
161173
}
162174

0 commit comments

Comments
 (0)