Skip to content

Commit fb163cb

Browse files
Daniel Schmidtthebuilder
authored andcommitted
Convert to Flow
1 parent 3c9c057 commit fb163cb

File tree

8 files changed

+143
-83
lines changed

8 files changed

+143
-83
lines changed

.flowconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[ignore]
2+
<PROJECT_ROOT>/dist/.*
3+
<PROJECT_ROOT>/node_modules/radium*
4+
<PROJECT_ROOT>/node_modules/babel*
5+
<PROJECT_ROOT>/node_modules/immutable*
6+
<PROJECT_ROOT>/node_modules/.*/test*
7+
8+
[include]
9+
10+
[libs]
11+
12+
[lints]
13+
14+
[options]
15+
module.system.node.resolve_dirname=node_modules
16+
module.system.node.resolve_dirname=src

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
],
2121
"scripts": {
2222
"build": "rm -rf lib && npm run build:lib && npm run build:es",
23-
"build:es": "BABEL_ENV=es babel src --out-dir es --ignore __*,*.story.js,*.test.js",
23+
"build:es": "BABEL_ENV=es babel src --out-dir es --ignore __*,*.story.js,*.test.js",
2424
"build:lib": "babel src --out-dir lib --ignore __*,*.story.js,*.test.js",
2525
"build:storybook": "build-storybook --output-dir example",
2626
"dev": "concurrently -k -r 'jest --watch' 'npm run storybook'",
2727
"lint": "eslint {src,stories,tests}/. ",
28-
"precommit": "lint-staged",
28+
"precommit": "flow && lint-staged",
2929
"postcommit": "git reset",
3030
"prepublish": "npm run build",
3131
"pretty": "prettier '{src,tests,stories}/**/*.js' --write --no-semi --single-quote --trailing-comma all",
@@ -59,7 +59,6 @@
5959
]
6060
},
6161
"dependencies": {
62-
"prop-types": "^15.6.0"
6362
},
6463
"peerDependencies": {
6564
"react": "^15.0.0 || ^16.0.0 || ^17.0.0"
@@ -80,6 +79,7 @@
8079
"enzyme-to-json": "^3.1.3",
8180
"eslint": "^4.9.0",
8281
"eslint-config-insilico": "^5.0.1",
82+
"flow-bin": "^0.57.3",
8383
"husky": "^0.14.3",
8484
"intersection-observer": "^0.4.0",
8585
"jest": "^21.2.1",

src/index.js

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
1-
import React, { Component, createElement } from 'react' // eslint-disable-line no-unused-vars
2-
import PropTypes from 'prop-types'
1+
// @flow
2+
import * as React from 'react'
33
import { observe, unobserve } from './intersection'
44

5-
const isFunction = func => typeof func === 'function'
5+
type Props = {
6+
/** Element tag to use for the wrapping */
7+
tag: string,
8+
/** Only trigger the inView callback once */
9+
triggerOnce: boolean,
10+
/** Children should be either a function or a node */
11+
children?: ((inView: boolean) => React.Node) | React.Node,
12+
/** Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. */
13+
threshold?: number | Array<number>,
14+
/** The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null.*/
15+
root?: HTMLElement,
16+
/** Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). */
17+
rootMargin?: string,
18+
/** Unique identifier for the root element - This is used to identify the IntersectionObserver instance, so it can be reused.
19+
* If you defined a root element, without adding an id, it will create a new instance for all components. */
20+
rootId?: string,
21+
/** Call this function whenever the in view state changes */
22+
onChange?: (inView: boolean) => void,
23+
/** Use render method to only render content when inView */
24+
render?: () => React.Node,
25+
}
26+
27+
type State = {
28+
inView: boolean,
29+
}
630

731
/**
832
* Monitors scroll, and triggers the children function with updated props
@@ -13,32 +37,7 @@ const isFunction = func => typeof func === 'function'
1337
)}
1438
</Observer>
1539
*/
16-
class Observer extends Component {
17-
static propTypes = {
18-
/** Element tag to use for the wrapping */
19-
tag: PropTypes.node,
20-
/** Children should be either a function or a node */
21-
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
22-
/** Only trigger the inView callback once */
23-
triggerOnce: PropTypes.bool,
24-
/** Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. */
25-
threshold: PropTypes.oneOfType([
26-
PropTypes.arrayOf(PropTypes.number),
27-
PropTypes.number,
28-
]),
29-
/** The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null.*/
30-
root: PropTypes.object,
31-
/** Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). */
32-
rootMargin: PropTypes.string,
33-
/** Unique identifier for the root element - This is used to identify the IntersectionObserver instance, so it can be reused.
34-
* If you defined a root element, without adding an id, it will create a new instance for all components. */
35-
rootId: PropTypes.string,
36-
/** Call this function whenever the in view state changes */
37-
onChange: PropTypes.func,
38-
/** Use render method to only render content when inView */
39-
render: PropTypes.func,
40-
}
41-
40+
class Observer extends React.Component<Props, State> {
4241
static defaultProps = {
4342
tag: 'div',
4443
threshold: 0,
@@ -49,13 +48,13 @@ class Observer extends Component {
4948
inView: false,
5049
}
5150

52-
componentWillUpdate(nextProps, nextState) {
51+
componentWillUpdate(nextProps: Props, nextState: State) {
5352
if (!!this.props.onChange && nextState !== this.state) {
5453
this.props.onChange(nextState.inView)
5554
}
5655
}
5756

58-
componentDidUpdate(prevProps, prevState) {
57+
componentDidUpdate(prevProps: Props, prevState: State) {
5958
// If a IntersectionObserver option changed, reinit the observer
6059
if (
6160
prevProps.rootMargin !== this.props.rootMargin ||
@@ -81,7 +80,7 @@ class Observer extends Component {
8180
}
8281
}
8382

84-
node = null
83+
node: ?HTMLElement = null
8584

8685
observeNode() {
8786
if (!this.node) return
@@ -98,13 +97,13 @@ class Observer extends Component {
9897
)
9998
}
10099

101-
handleNode = node => {
100+
handleNode = (node: ?HTMLElement) => {
102101
if (this.node) unobserve(this.node)
103102
this.node = node
104103
this.observeNode()
105104
}
106105

107-
handleChange = inView => this.setState({ inView })
106+
handleChange = (inView: boolean) => this.setState({ inView })
108107

109108
render() {
110109
const {
@@ -121,17 +120,17 @@ class Observer extends Component {
121120

122121
const { inView } = this.state
123122

124-
return createElement(
123+
return React.createElement(
125124
tag,
126125
{
127126
...props,
128127
ref: this.handleNode,
129128
},
130129
// If render is a function, use it to render content when in view
131-
inView && isFunction(render) ? render() : null,
132-
// If children is a function, render it with the current inView status.
133-
// Otherwise always render children. Assume onChange is being used outside, to control the the state of children.
134-
isFunction(children) ? children(inView) : children,
130+
inView && typeof render === 'function' ? render() : null,
131+
// // If children is a function, render it with the current inView status.
132+
// // Otherwise always render children. Assume onChange is being used outside, to control the the state of children.
133+
typeof children === 'function' ? children(inView) : children,
135134
)
136135
}
137136
}

src/intersection.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
const INSTANCE_MAP = new Map()
2-
const OBSERVER_MAP = new Map()
1+
// @flow
2+
type Callback = (inView: boolean) => void
3+
4+
type Instance = {
5+
callback: Callback,
6+
visible: boolean,
7+
options: IntersectionObserverOptions,
8+
observerId: ?string,
9+
observer: ?IntersectionObserver,
10+
}
11+
12+
const INSTANCE_MAP: Map<HTMLElement, Instance> = new Map()
13+
const OBSERVER_MAP: Map<?string, IntersectionObserver> = new Map()
314

415
/**
516
* Monitor element, and trigger callback when element becomes visible
@@ -12,16 +23,19 @@ const OBSERVER_MAP = new Map()
1223
* @param rootId {String} Unique identifier for the root element, to enable reusing the IntersectionObserver
1324
*/
1425
export function observe(
15-
element,
16-
callback,
17-
options = {
26+
element: HTMLElement,
27+
callback: Callback,
28+
options: IntersectionObserverOptions = {
1829
threshold: 0,
1930
},
20-
rootId = null,
31+
rootId?: string,
2132
) {
22-
const { threshold, root, rootMargin } = options
33+
const { root, rootMargin } = options
34+
const threshold = options.threshold || 0
2335
if (!element || !callback) return
24-
let observerId = rootMargin ? `${threshold}_${rootMargin}` : `${threshold}`
36+
let observerId = rootMargin
37+
? `${threshold.toString()}_${rootMargin}`
38+
: `${threshold.toString()}`
2539

2640
if (root) {
2741
observerId = rootId ? `${rootId}_${observerId}` : null
@@ -33,7 +47,7 @@ export function observe(
3347
if (observerId) OBSERVER_MAP.set(observerId, observerInstance)
3448
}
3549

36-
const instance = {
50+
const instance: Instance = {
3751
callback,
3852
visible: false,
3953
options,
@@ -53,16 +67,18 @@ export function observe(
5367
* make sure to call this method.
5468
* @param element {HTMLElement}
5569
*/
56-
export function unobserve(element) {
70+
export function unobserve(element: ?HTMLElement) {
5771
if (!element) return
72+
const instance = INSTANCE_MAP.get(element)
5873

59-
if (INSTANCE_MAP.has(element)) {
60-
const { observerId, observer } = INSTANCE_MAP.get(element)
74+
if (instance) {
75+
const { observerId, observer } = instance
6176
const observerInstance = observerId
6277
? OBSERVER_MAP.get(observerId)
6378
: observer
6479

6580
if (observerInstance) {
81+
// $FlowFixMe - the interface in bom.js is wrong. Spec should accept the element.
6682
observerInstance.unobserve(element)
6783
}
6884

@@ -101,12 +117,12 @@ export function destroy() {
101117

102118
function onChange(changes) {
103119
changes.forEach(intersection => {
104-
if (INSTANCE_MAP.has(intersection.target)) {
105-
const { isIntersecting, intersectionRatio, target } = intersection
106-
const instance = INSTANCE_MAP.get(target)
120+
const { isIntersecting, intersectionRatio, target } = intersection
121+
const instance = INSTANCE_MAP.get(target)
122+
if (instance) {
107123
const options = instance.options
108124

109-
let inView
125+
let inView = false
110126

111127
if (Array.isArray(options.threshold)) {
112128
// If threshold is an array, check if any of them intersects. This just triggers the onChange event multiple times.
@@ -115,7 +131,7 @@ function onChange(changes) {
115131
? intersectionRatio > threshold
116132
: intersectionRatio >= threshold
117133
})
118-
} else {
134+
} else if (options.threshold !== undefined) {
119135
// Trigger on 0 ratio only when not visible. This is fallback for browsers without isIntersecting support
120136
inView = instance.visible
121137
? intersectionRatio > options.threshold

0 commit comments

Comments
 (0)