├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── example
├── .eslintrc.yml
├── WithMousePosition.js
├── WithOffsetToRoot.js
├── WithSize.js
├── WithWindowSize.js
├── entry.js
├── index.html
├── webpack.config.js
└── webpack
│ └── development.config.js
├── package.json
├── src
├── getOffsetToRoot.js
├── mapPropsOnEvent.js
├── mapPropsOnScroll.js
├── withMousePosition.js
├── withOffsetToRoot.js
├── withSize.js
└── withWindowSize.js
├── test
├── .eslintrc.yml
├── getOffsetToRoot.test.js
├── mapPropsOnEvent.test.js
├── mapPropsOnScroll.test.js
├── setupEnzyme.js
├── setupJsdom.js
├── withMousePosition.test.js
├── withOffsetToRoot.test.js
├── withSize.test.js
└── withWindowSize.test.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | .nyc_output
4 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | root: true
3 | extends: react-app
4 | parser: babel-eslint
5 | env:
6 | browser: true
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | npm-debug.log*
3 |
4 | # Coverage
5 | .nyc_output
6 |
7 | # Dependency directory
8 | node_modules
9 |
10 | # Build
11 | lib
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | example
3 | test
4 | .babelrc
5 | .editorconfig
6 | .eslintignore
7 | .eslintrc
8 | .npmignore
9 | .stylelintrc
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | before_install:
5 | - npm install -g npm
6 | script:
7 | - npm run test:coverage
8 | after_success:
9 | - npm run coverage
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## HEAD
2 |
3 | - Update recompose to 0.26.0
4 |
5 | ## [v2.0.1]
6 |
7 | - Add recompose@0.21 to peerDependencies
8 | - Add yarn.lock
9 | - Fix a typo in README
10 |
11 | ## [v2.0.0]
12 |
13 | - Update recompose to 0.20
14 | - Remove all default parameters, including `throttle` and `mapOnMount`
15 |
16 | [v2.0.0]: https://github.com/wuct/react-dom-utils/compare/v2.0.0...v1.4.0
17 |
18 | ## [v1.4.0]
19 | > April 29, 2016
20 |
21 | - feat(withSize): cancel throttle on unmounting
22 | - feat(withSize): append `size` after mounting
23 | - feat(mapPropsOnScroll): support for mapPropsOnScroll
24 | - feat(mapPropsOnEvent): support for throttle canceling
25 | - fix(withNousePosition): handle throttling correctly
26 | - feat(withNousePosition): reset to default state when mouse leaving
27 | - feat(withNousePosition): support for throttle canceling
28 |
29 | [v1.4.0]: https://github.com/wuct/raf-throttle/compare/v1.4.0...v1.3.0
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 吳敬庭
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-dom-utils
2 |
3 | [](https://www.npmjs.com/package/react-dom-utils)
4 | [](https://travis-ci.org/wuct/react-dom-utils)
5 | [](https://codecov.io/github/wuct/react-dom-utils)
6 | [](https://codeclimate.com/github/wuct/react-dom-utils)
7 |
8 | Inspired [recompose](https://github.com/acdlite/recompose/), [react-dom-utils](https://www.npmjs.com/package/react-dom-utils) let you work with DOMs in HOCs.
9 |
10 | We love functional stateless components, but when it comes to `findDOMNode`, we are forced to use class components. `react-dom-utils` let you lift your `findDOMNode` related jobs into hight-order components and write more small, reactive functional components.
11 |
12 | You can use `react-dom-utils` to
13 |
14 | * Get `window`'s width and height, and get updated when `window` resizes
15 | * Get `keyCode`s when `document` or another DOM element receives `keyDown` events
16 | * Get `pageX` and `pageY` from a `mousemove` event
17 |
18 | ... and more.
19 |
20 |
21 | ## Installation
22 |
23 | `npm install react-dom-utils --save`
24 |
25 | ## Example
26 |
27 | ```js
28 | import React from 'react'
29 | import withMousePosition from 'react-dom-utils/lib/withMousePosition.js'
30 | import throttle from 'raf-throttle'
31 |
32 | // withMousePosition appends a mousePosition object to the base component props
33 | const enhance = withMousePosition(throttle)
34 |
35 | const component = ({ mousePosition: { pageX, pageY } }) =>
36 |
37 | Follow your mouse
38 |
39 |
40 | export default enhance(component)
41 | ```
42 |
43 | More examples is [here](https://github.com/wuct/react-dom-utils/tree/master/example)
44 |
45 |
46 | ## Usage
47 | ### `throttle`
48 | The throttling function is for throttling DOM events. It is recommended to use [raf-throttle](https://github.com/wuct/raf-throttle) which throttles DOM events by `requestAnimationFrame`. However, you can pass in an [identity](https://lodash.com/docs#identity) function if you do not want throttling.
49 |
50 | ## API
51 |
52 | Docs are annotated using Flow type notation, given the following types:
53 |
54 | ```js
55 | type ReactElementType = Class | StatelessFunctionComponent | string
56 | ```
57 |
58 | ### `mapPropsOnEvent()`
59 |
60 | ```js
61 | mapPropsOnEvent(
62 | getTarget: (component: ReactComponent) => DOMEventTarget
63 | type: string,
64 | propsMapper: (event: DOMEvent, component: ReactComponent) => Object,
65 | throttle: Function,
66 | mapOnMount: boolean,
67 | BaseComponent: ReactElementType
68 | ): ReactElementType
69 | ```
70 |
71 | Attaches the props returned by `propsMapper` to owner props and updates it when the specified event is triggered.
72 |
73 | ### `withMousePosition()`
74 |
75 | ```js
76 | withMousePosition(
77 | throttle: Function
78 | ): ReactElementType
79 | ```
80 |
81 | Attaches `mousePosition` to owner props and updates it when a `mouseover` event of the base component is triggered.
82 |
83 | `mousePosition` has the following signature:
84 |
85 | ```js
86 | {
87 | pageX: number,
88 | pageY: number,
89 | clientX: number,
90 | clientY: number,
91 | screenX: number,
92 | screenY: number
93 | }
94 | ```
95 |
96 | ### `withSize()`
97 |
98 |
99 | ```js
100 | withSize(
101 | throttle: Function
102 | ): ReactElementType
103 | ```
104 |
105 | Attaches `DOMSize` to owner props and updates it when a `resize` event (detected by [element-resize-detector](https://github.com/wnr/element-resize-detector)) of the base component is triggered.
106 |
107 | `DOMSize` has the following signature:
108 |
109 | ```js
110 | {
111 | offsetWidth: number,
112 | offsetHeight: number,
113 | clientWidth: number,
114 | clientHeight: number,
115 | scrollWidth: number,
116 | scrollHeight: number
117 | }
118 | ```
119 |
120 | ### `withWindowSize()`
121 |
122 | ```js
123 | withWindowSize(
124 | throttle: Function
125 | ): ReactElementType
126 | ```
127 |
128 | Attaches `windowSize` to owner props and updates it when a `resize` event of `window` is triggered.
129 |
130 | `windowSize` has the following signature:
131 |
132 | ```js
133 | {
134 | innerWidth: number,
135 | innerHeight: number,
136 | outerWidth: number,
137 | outerHeight: number
138 | }
139 | ```
140 |
141 | ### `withOffsetToRoot()`
142 |
143 |
144 | ```js
145 | withOffsetToRoot(
146 | throttle: Function
147 | ): ReactElementType
148 | ```
149 |
150 | Attaches `offsetToRoot` to owner props and updates it when a `resize` event of `window` is triggered.
151 |
152 | `offsetToRoot` has the following signature:
153 |
154 | ```js
155 | {
156 | offsetTop: number,
157 | offsetLeft: number
158 | }
159 | ```
160 |
161 | ## `mapPropsOnScroll()`
162 |
163 | ```js
164 | type Scroll = {
165 | x: number,
166 | y: number,
167 | };
168 |
169 | mapPropsOnScroll(
170 | propsMapper: (scroll: Scroll, previousScroll: Scroll) => Object,
171 | throttle: Function,
172 | BaseComponent: ReactElementType
173 | ): ReactElementType
174 | ```
175 |
176 | Attaches the props returned by propsMapper to owner props and updates it when a `scroll` event of the `window` is triggered.
177 |
178 | Example:
179 |
180 | ```js
181 | mapPropsOnScroll((scroll, previousScroll) => ({
182 | isScrollUp: previousScroll.y > scroll.y,
183 | })),
184 | ```
185 |
186 | ## Contributing
187 |
188 | 1. Fork it
189 | 2. Create your feature branch (`git checkout -b my-new-feature`)
190 | 3. Commit your changes (`git commit -am 'Add some feature'`)
191 | 4. Push to the branch (`git push origin my-new-feature`)
192 | 5. Create new Pull Request
193 |
--------------------------------------------------------------------------------
/example/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | import/no-extraneous-dependencies:
4 | - 2
5 | - devDependencies: true
6 | react/prop-types:
7 | - 0
8 |
--------------------------------------------------------------------------------
/example/WithMousePosition.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import throttle from 'raf-throttle'
3 | import withMousePosition from '../src/withMousePosition'
4 |
5 | const style = {
6 | width: 400,
7 | height: 400,
8 | backgroundColor: '#ECBDBB',
9 | }
10 |
11 | const component = ({ mousePosition }) => (
12 |
13 | {JSON.stringify(mousePosition)}
14 |
15 | )
16 |
17 | export default withMousePosition(throttle)(component)
18 |
--------------------------------------------------------------------------------
/example/WithOffsetToRoot.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import throttle from 'raf-throttle'
3 | import withOffsetToRoot from '../src/withOffsetToRoot'
4 |
5 | const style = {
6 | width: 200,
7 | height: 200,
8 | backgroundColor: '#93626A',
9 | }
10 |
11 | const component = ({ offsetToRoot }) => (
12 |
13 | {JSON.stringify(offsetToRoot)}
14 |
15 | )
16 |
17 | export default withOffsetToRoot(throttle)(component)
18 |
--------------------------------------------------------------------------------
/example/WithSize.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import throttle from 'raf-throttle'
3 | import withSize from '../src/withSize'
4 |
5 | const style = {
6 | width: '100%',
7 | height: 100,
8 | backgroundColor: '#7E94C7',
9 | }
10 |
11 | const component = ({ DOMSize }) =>
12 | (
13 |
14 | {JSON.stringify(DOMSize)}
15 |
16 |
)
17 |
18 | export default withSize(throttle)(component)
19 |
--------------------------------------------------------------------------------
/example/WithWindowSize.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import throttle from 'raf-throttle'
3 | import withWindowSize from '../src/withWindowSize'
4 |
5 | const style = {
6 | width: '100%',
7 | height: 100,
8 | backgroundColor: '#6D695B',
9 | }
10 |
11 | const component = ({ windowSize }) =>
12 | (
13 |
14 | {JSON.stringify(windowSize)}
15 |
16 |
)
17 |
18 | export default withWindowSize(throttle)(component)
19 |
--------------------------------------------------------------------------------
/example/entry.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import WithMousePosition from './WithMousePosition'
4 | import WithSize from './WithSize'
5 | import WithWindowSize from './WithWindowSize'
6 | import WithOffsetToRoot from './WithOffsetToRoot'
7 |
8 | render(
9 |
10 |
11 |
12 |
13 |
14 |
,
15 | document.querySelector('#mountNode'),
16 | )
17 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Webpack App
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 |
3 | module.exports = require('./webpack/development.config')
4 |
--------------------------------------------------------------------------------
/example/webpack/development.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import webpack from 'webpack'
3 | import HtmlWebpackPlugin from 'html-webpack-plugin'
4 |
5 | export default {
6 | entry: [
7 | 'webpack-dev-server/client?http://localhost:8080',
8 | 'webpack/hot/dev-server',
9 | path.resolve(__dirname, '../entry.js'),
10 | ],
11 | output: {
12 | path: path.resolve(__dirname, '../build/'),
13 | filename: '[name].js',
14 | publicPath: '/',
15 | },
16 |
17 | module: {
18 | loaders: [
19 | {
20 | test: /\.js$/,
21 | exclude: /node_modules/,
22 | loaders: ['babel-loader'],
23 | },
24 | ],
25 | },
26 |
27 | devtool: 'sourcemap',
28 |
29 | plugins: [
30 | new HtmlWebpackPlugin({
31 | template: path.resolve(__dirname, '../index.html'),
32 | }),
33 | new webpack.HotModuleReplacementPlugin(),
34 | ],
35 |
36 | resolve: {
37 | alias: {
38 | react: path.resolve(__dirname, '../../node_modules/react'),
39 | 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom'),
40 | },
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-dom-utils",
3 | "version": "2.0.2",
4 | "description": "DOM operation utilities for React",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "watch": "webpack-dev-server --config ./example/webpack.config.js",
8 | "lint": "eslint ./",
9 | "test": "NODE_ENV=test npm run lint && NODE_ENV=test jest",
10 | "test:coverage": "nyc npm run test",
11 | "coverage": "nyc report --reporter=lcov > coverage.lcov && codecov",
12 | "build": "NODE_ENV=production babel src --out-dir lib",
13 | "prepublish": "npm run build",
14 | "precommit": "lint-staged"
15 | },
16 | "lint-staged": {
17 | "*.js": [
18 | "prettier --write",
19 | "git add"
20 | ]
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/wuct/react-dom-utils.git"
25 | },
26 | "keywords": [
27 | "react",
28 | "dom",
29 | "utilities",
30 | "hoc",
31 | "high-order",
32 | "components",
33 | "toolkit"
34 | ],
35 | "author": "wuct",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/wuct/react-dom-utils/issues"
39 | },
40 | "homepage": "https://github.com/wuct/react-dom-utils#readme",
41 | "babel": {
42 | "presets": [
43 | "react-app"
44 | ]
45 | },
46 | "jest": {
47 | "setupFiles": [
48 | "babel-register",
49 | "./test/setupJsdom.js",
50 | "./test/setupEnzyme.js"
51 | ]
52 | },
53 | "dependencies": {
54 | "element-resize-detector": "^1.1.0",
55 | "lodash": "^4.6.1"
56 | },
57 | "devDependencies": {
58 | "babel-cli": "^6.6.5",
59 | "babel-core": "^6.7.4",
60 | "babel-eslint": "8.2.2",
61 | "babel-loader": "^7.0.0",
62 | "babel-preset-react-app": "^3.1.0",
63 | "babel-register": "^6.7.2",
64 | "codecov": "^3.0.0",
65 | "enzyme": "^3.1.0",
66 | "enzyme-adapter-react-16": "^1.0.4",
67 | "eslint": "^5.0.0",
68 | "eslint-config-react-app": "^2.0.0",
69 | "eslint-plugin-flowtype": "^2.33.0",
70 | "eslint-plugin-import": "^2.2.0",
71 | "eslint-plugin-jsx-a11y": "^6.0.3",
72 | "eslint-plugin-react": "^7.0.1",
73 | "expect": "^1.18.0",
74 | "html-webpack-plugin": "^3.0.0",
75 | "husky": "^0.14.3",
76 | "jest": "^22.0.0",
77 | "jsdom": "^11.0.0",
78 | "lint-staged": "^7.0.0",
79 | "nyc": "^11.0.3",
80 | "prettier": "^1.5.3",
81 | "raf-throttle": "^2.0.2",
82 | "react": "^16.0.0",
83 | "react-dom": "^16.0.0",
84 | "recompose": "^0.26.0",
85 | "simulant": "^0.2.0",
86 | "webpack": "^4.5.0",
87 | "webpack-dev-server": "^3.0.0"
88 | },
89 | "peerDependencies": {
90 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0",
91 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0",
92 | "recompose": "^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.26.0"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/getOffsetToRoot.js:
--------------------------------------------------------------------------------
1 | /* eslint no-param-reassign: 0 */
2 | const getOffsetToRoot = element => {
3 | let left = 0;
4 | let top = 0;
5 |
6 | while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
7 | left += element.offsetLeft - element.scrollLeft;
8 | top += element.offsetTop - element.scrollTop;
9 | element = element.offsetParent;
10 | }
11 |
12 | return { offsetTop: top, offsetLeft: left };
13 | };
14 |
15 | export default getOffsetToRoot;
16 |
--------------------------------------------------------------------------------
/src/mapPropsOnEvent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import wrapDisplayName from "recompose/wrapDisplayName";
3 | import setDisplayName from "recompose/setDisplayName";
4 | import isFunction from "lodash/isFunction";
5 |
6 | const mapPropsOnEvent = (
7 | getTarget,
8 | type,
9 | propsMapper,
10 | throttle,
11 | mapOnMount
12 | ) => BaseComponent => {
13 | class MapPropsOnEvent extends React.Component {
14 | state = {};
15 |
16 | componentDidMount = () => {
17 | this.target = getTarget(this);
18 | this.target.addEventListener(type, this.mapProps);
19 |
20 | if (mapOnMount) {
21 | this.mapProps();
22 | }
23 | };
24 |
25 | componentWillUnmount = () => {
26 | if (isFunction(this.mapProps.cancel)) {
27 | this.mapProps.cancel();
28 | }
29 |
30 | if (this.target) {
31 | this.target.removeEventListener(type, this.mapProps);
32 | }
33 | };
34 |
35 | mapProps = throttle(e => this.setState(propsMapper(e, this)));
36 |
37 | render = () =>
38 | React.createElement(BaseComponent, {
39 | ...this.props,
40 | ...this.state
41 | });
42 | }
43 |
44 | if (process.env.NODE_ENV !== "production") {
45 | return setDisplayName(wrapDisplayName(BaseComponent, "mapPropsOnEvent"))(
46 | MapPropsOnEvent
47 | );
48 | }
49 |
50 | return MapPropsOnEvent;
51 | };
52 |
53 | export default mapPropsOnEvent;
54 |
--------------------------------------------------------------------------------
/src/mapPropsOnScroll.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import wrapDisplayName from "recompose/wrapDisplayName";
3 | import setDisplayName from "recompose/setDisplayName";
4 | import isFunction from "lodash/isFunction";
5 |
6 | const getScroll = () => ({
7 | x: window.pageXOffset,
8 | y: window.pageYOffset
9 | });
10 |
11 | const mapPropsOnScroll = (propsMapper, throttle) => BaseComponent => {
12 | class MapPropsOnScroll extends React.Component {
13 | scroll = {};
14 |
15 | componentDidMount = () => {
16 | this.scroll = getScroll();
17 | window.addEventListener("scroll", this.mapProps);
18 | };
19 |
20 | componentWillUnmount = () => {
21 | if (isFunction(this.mapProps.cancel)) {
22 | this.mapProps.cancel();
23 | }
24 |
25 | window.removeEventListener("scroll", this.mapProps);
26 | };
27 |
28 | mapProps = throttle(() => {
29 | // Remind: fix for safari over scrolling problem
30 | const maxY = document.body.offsetHeight - window.innerHeight;
31 | if (
32 | document.body.offsetHeight !== 0 && // offsetHeight is always zero in jsdom
33 | (getScroll().y < 0 || getScroll().y > maxY)
34 | ) {
35 | return;
36 | }
37 |
38 | this.setState(() => propsMapper(getScroll(), this.scroll));
39 | this.scroll = getScroll();
40 | });
41 |
42 | render = () =>
43 | React.createElement(BaseComponent, {
44 | ...this.props,
45 | ...this.state
46 | });
47 | }
48 |
49 | if (process.env.NODE_ENV !== "production") {
50 | return setDisplayName(wrapDisplayName(BaseComponent, "mapPropsOnScroll"))(
51 | MapPropsOnScroll
52 | );
53 | }
54 |
55 | return MapPropsOnScroll;
56 | };
57 |
58 | export default mapPropsOnScroll;
59 |
--------------------------------------------------------------------------------
/src/withMousePosition.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { findDOMNode } from "react-dom";
3 | import wrapDisplayName from "recompose/wrapDisplayName";
4 | import setDisplayName from "recompose/setDisplayName";
5 | import pick from "lodash/pick";
6 | import isFunction from "lodash/isFunction";
7 |
8 | const pickedProps = [
9 | "pageX",
10 | "pageY",
11 | "clientX",
12 | "clientY",
13 | "screenX",
14 | "screenY"
15 | ];
16 |
17 | export const defaultState = { mousePosition: undefined };
18 |
19 | const withMousePosition = throttle => BaseComponent => {
20 | class WithMousePosition extends React.Component {
21 | state = defaultState;
22 |
23 | componentDidMount = () => {
24 | this.dom = findDOMNode(this);
25 | this.dom.addEventListener("mousemove", this.onMouseMove);
26 | this.dom.addEventListener("mouseleave", this.onMouseLeave);
27 | };
28 |
29 | componentWillUnmount = () => {
30 | if (isFunction(this.onMouseMove.cancel)) {
31 | this.onMouseMove.cancel();
32 | }
33 |
34 | this.dom.removeEventListener("mousemove", this.onMouseMove);
35 | this.dom.removeEventListener("mouseleave", this.onMouseLeave);
36 | };
37 |
38 | onMouseMove = throttle(e =>
39 | this.setState({ mousePosition: pick(e, pickedProps) })
40 | );
41 |
42 | onMouseLeave = () => this.setState(defaultState);
43 |
44 | render = () =>
45 | React.createElement(BaseComponent, {
46 | ...this.props,
47 | ...this.state
48 | });
49 | }
50 |
51 | if (process.env.NODE_ENV !== "production") {
52 | return setDisplayName(wrapDisplayName(BaseComponent, "withMousePosition"))(
53 | WithMousePosition
54 | );
55 | }
56 |
57 | return WithMousePosition;
58 | };
59 |
60 | export default withMousePosition;
61 |
--------------------------------------------------------------------------------
/src/withOffsetToRoot.js:
--------------------------------------------------------------------------------
1 | import { findDOMNode } from "react-dom";
2 | import wrapDisplayName from "recompose/wrapDisplayName";
3 | import setDisplayName from "recompose/setDisplayName";
4 | import mapPropsOnEvent from "./mapPropsOnEvent";
5 | import getOffsetToRoot from "./getOffsetToRoot";
6 |
7 | const withOffsetToRoot = throttle => BaseComponent => {
8 | const WithOffsetToRoot = mapPropsOnEvent(
9 | () => window,
10 | "resize",
11 | (e, self) => ({ offsetToRoot: getOffsetToRoot(findDOMNode(self)) }),
12 | throttle,
13 | true
14 | )(BaseComponent);
15 |
16 | if (process.env.NODE_ENV !== "production") {
17 | return setDisplayName(wrapDisplayName(BaseComponent, "withMousePosition"))(
18 | WithOffsetToRoot
19 | );
20 | }
21 |
22 | return WithOffsetToRoot;
23 | };
24 |
25 | export default withOffsetToRoot;
26 |
--------------------------------------------------------------------------------
/src/withSize.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { findDOMNode } from "react-dom";
3 | import wrapDisplayName from "recompose/wrapDisplayName";
4 | import setDisplayName from "recompose/setDisplayName";
5 | import pick from "lodash/pick";
6 | import isFunction from "lodash/isFunction";
7 |
8 | const pickedProps = [
9 | "offsetWidth",
10 | "offsetHeight",
11 | "clientWidth",
12 | "clientHeight",
13 | "scrollWidth",
14 | "scrollHeight"
15 | ];
16 |
17 | const withSize = throttle => BaseComponent => {
18 | class WithSize extends React.Component {
19 | state = {};
20 |
21 | /*
22 | * The erd will append an object element to the DOM.
23 | * Before react@15, if there is a string without a wrapping
24 | * element inside your component, ex: () => foo
,
25 | * react will create a for you. This behavior will
26 | * cause the erd not working. Make sure to wrap your strings,
27 | * ex: () => foo
.
28 | */
29 |
30 | componentDidMount = () => {
31 | /* eslint-disable global-require */
32 | this.erd = require("element-resize-detector")();
33 | /* eslint-enable global-require */
34 |
35 | this.erd.listenTo(findDOMNode(this), this.onResize);
36 |
37 | this.setSizeToState();
38 | };
39 |
40 | componentWillUnmount = () => {
41 | if (isFunction(this.onResize.cancel)) {
42 | this.onResize.cancel();
43 | }
44 |
45 | this.erd.removeListener(findDOMNode(this), this.onResize);
46 | };
47 |
48 | setSizeToState = () =>
49 | this.setState({ DOMSize: pick(findDOMNode(this), pickedProps) });
50 |
51 | onResize = throttle(this.setSizeToState);
52 |
53 | render = () =>
54 | React.createElement(BaseComponent, {
55 | ...this.props,
56 | ...this.state
57 | });
58 | }
59 |
60 | if (process.env.NODE_ENV !== "production") {
61 | return setDisplayName(wrapDisplayName(BaseComponent, "withSize"))(WithSize);
62 | }
63 |
64 | return WithSize;
65 | };
66 |
67 | export default withSize;
68 |
--------------------------------------------------------------------------------
/src/withWindowSize.js:
--------------------------------------------------------------------------------
1 | import wrapDisplayName from "recompose/wrapDisplayName";
2 | import setDisplayName from "recompose/setDisplayName";
3 | import pick from "lodash/pick";
4 | import mapPropsOnEvent from "./mapPropsOnEvent";
5 |
6 | export const pickedProps = [
7 | "innerWidth",
8 | "innerHeight",
9 | "outerWidth",
10 | "outerHeight"
11 | ];
12 |
13 | const withWindowSize = throttle => BaseComponent => {
14 | const WithWindowSize = mapPropsOnEvent(
15 | () => window,
16 | "resize",
17 | () => ({ windowSize: pick(window, pickedProps) }),
18 | throttle,
19 | true
20 | )(BaseComponent);
21 |
22 | if (process.env.NODE_ENV !== "production") {
23 | return setDisplayName(wrapDisplayName(BaseComponent, "withWindowSize"))(
24 | WithWindowSize
25 | );
26 | }
27 |
28 | return WithWindowSize;
29 | };
30 |
31 | export default withWindowSize;
32 |
--------------------------------------------------------------------------------
/test/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | rules:
3 | import/no-extraneous-dependencies:
4 | - 2
5 | - devDependencies: true
6 |
--------------------------------------------------------------------------------
/test/getOffsetToRoot.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 |
5 | import getOffsetToRoot from "../src/getOffsetToRoot";
6 |
7 | // jsdom still has not supported offsetTop adn offsetLeft, yet,
8 | // so we use this hack currenty.
9 | // see: https://github.com/tmpvar/jsdom/issues/135
10 |
11 | Object.defineProperties(window.HTMLElement.prototype, {
12 | offsetLeft: {
13 | get() {
14 | return parseFloat(window.getComputedStyle(this).marginLeft) || 0;
15 | }
16 | },
17 | offsetTop: {
18 | get() {
19 | return parseFloat(window.getComputedStyle(this).marginTop) || 0;
20 | }
21 | },
22 | scrollLeft: {
23 | get() {
24 | return 0;
25 | }
26 | },
27 | scrollTop: {
28 | get() {
29 | return 0;
30 | }
31 | },
32 | offsetParent: {
33 | get() {
34 | return this.parentNode;
35 | }
36 | }
37 | });
38 |
39 | class Div extends React.Component {
40 | render() {
41 | return {this.props.children}
;
42 | }
43 | }
44 |
45 | test("one level DOM tree", () => {
46 | const wrapper = mount();
47 | const dom = wrapper.getDOMNode();
48 |
49 | expect(getOffsetToRoot(dom)).toEqual({ offsetTop: 10, offsetLeft: 10 });
50 | });
51 |
52 | test("two levels DOM tree", () => {
53 | const wrapper = mount(
54 |
57 | );
58 |
59 | const dom = wrapper
60 | .find(Div)
61 | .last()
62 | .getDOMNode();
63 |
64 | expect(getOffsetToRoot(dom)).toEqual({ offsetTop: 4, offsetLeft: 6 });
65 | });
66 |
--------------------------------------------------------------------------------
/test/mapPropsOnEvent.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { findDOMNode } from "react-dom";
3 | import { mount } from "enzyme";
4 | import expect from "expect";
5 | import simulant from "simulant";
6 |
7 | import mapPropsOnEvent from "../src/mapPropsOnEvent";
8 |
9 | class Div extends React.Component {
10 | render() {
11 | return ;
12 | }
13 | }
14 |
15 | test("map props on window's events", () => {
16 | const mapSpy = expect.createSpy().andReturn({ foo: "bar" });
17 |
18 | const Container = mapPropsOnEvent(
19 | () => window,
20 | "resize",
21 | mapSpy,
22 | f => f,
23 | false
24 | )(Div);
25 |
26 | const wrapper = mount();
27 |
28 | simulant.fire(window, "resize");
29 | expect(mapSpy.calls.length).toEqual(1);
30 |
31 | expect(wrapper.find(Div).instance().props).toEqual({ foo: "bar" });
32 |
33 | simulant.fire(window, "resize");
34 | expect(mapSpy.calls.length).toEqual(2);
35 |
36 | wrapper.unmount();
37 |
38 | simulant.fire(window, "resize");
39 | expect(mapSpy.calls.length).toEqual(2);
40 | });
41 |
42 | test("map props on dom's events", () => {
43 | const mapSpy = expect.createSpy().andReturn({ foo: "bar" });
44 |
45 | const Container = mapPropsOnEvent(
46 | self => findDOMNode(self),
47 | "click",
48 | mapSpy,
49 | f => f,
50 | false
51 | )(Div);
52 |
53 | const wrapper = mount();
54 | const dom = wrapper.getDOMNode();
55 |
56 | simulant.fire(dom, "click");
57 | expect(mapSpy.calls.length).toEqual(1);
58 |
59 | expect(wrapper.find(Div).instance().props).toEqual({ foo: "bar" });
60 |
61 | simulant.fire(dom, "click");
62 | expect(mapSpy.calls.length).toEqual(2);
63 |
64 | wrapper.unmount();
65 |
66 | simulant.fire(dom, "click");
67 | expect(mapSpy.calls.length).toEqual(2);
68 | });
69 |
--------------------------------------------------------------------------------
/test/mapPropsOnScroll.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 | import simulant from "simulant";
5 |
6 | import mapPropsOnScroll from "../src/mapPropsOnScroll";
7 | class Null extends React.Component {
8 | render() {
9 | return null;
10 | }
11 | }
12 |
13 | test("map props on window's scroll event", () => {
14 | const mapSpy = expect.createSpy().andReturn({ foo: "bar" });
15 |
16 | const Container = mapPropsOnScroll(mapSpy, f => f)(Null);
17 |
18 | const wrapper = mount();
19 |
20 | simulant.fire(window, "scroll");
21 | expect(mapSpy.calls.length).toEqual(1);
22 | expect(wrapper.find(Null).instance().props).toEqual({ foo: "bar" });
23 |
24 | simulant.fire(window, "scroll");
25 | expect(mapSpy.calls.length).toEqual(2);
26 |
27 | wrapper.unmount();
28 |
29 | simulant.fire(window, "scroll");
30 | expect(mapSpy.calls.length).toEqual(2);
31 | });
32 |
--------------------------------------------------------------------------------
/test/setupEnzyme.js:
--------------------------------------------------------------------------------
1 | const { configure } = require("enzyme");
2 | const Adapter = require("enzyme-adapter-react-16");
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/test/setupJsdom.js:
--------------------------------------------------------------------------------
1 | const jsdom = require("jsdom");
2 |
3 | const { JSDOM } = jsdom;
4 |
5 | const dom = new JSDOM("");
6 |
7 | global.window = dom.window;
8 | global.document = dom.window.document;
9 | global.navigator = dom.window.navigator;
10 | global.requestAnimationFrame = callback => {
11 | setTimeout(callback, 0);
12 | };
13 |
--------------------------------------------------------------------------------
/test/withMousePosition.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 | import simulant from "simulant";
5 | import identity from "lodash/identity";
6 |
7 | import withMousePosition, { defaultState } from "../src/withMousePosition";
8 |
9 | class Div extends React.Component {
10 | render() {
11 | return ;
12 | }
13 | }
14 |
15 | test("append mousePosition when mousemove", () => {
16 | const Container = withMousePosition(f => f)(Div);
17 | const wrapper = mount();
18 | const dom = wrapper.getDOMNode();
19 |
20 | simulant.fire(dom, "mousemove", { screenX: 1 });
21 |
22 | expect(wrapper.find(Div).instance().props).toInclude({
23 | mousePosition: { screenX: 1 }
24 | });
25 | });
26 |
27 | test("reset mousePosition to default when mouseleave", () => {
28 | const Container = withMousePosition(f => f)(Div);
29 | const wrapper = mount();
30 | const dom = wrapper.getDOMNode();
31 |
32 | simulant.fire(dom, "mousemove");
33 | simulant.fire(dom, "mouseleave");
34 |
35 | expect(wrapper.find(Div).instance().props).toEqual(defaultState);
36 | });
37 |
38 | test("invoke the provided throttle function only once", () => {
39 | const throttleSpy = expect.createSpy().andCall(identity);
40 | const Container = withMousePosition(throttleSpy)(Div);
41 | const wrapper = mount();
42 | const dom = wrapper.getDOMNode();
43 |
44 | simulant.fire(dom, "mousemove");
45 | simulant.fire(dom, "mouseleave");
46 | wrapper.unmount();
47 |
48 | expect(throttleSpy.calls.length).toEqual(1);
49 | });
50 |
51 | test("invoke the cancel function of the provided throttle when unmount", () => {
52 | const cancelSpy = expect.createSpy();
53 |
54 | /* eslint no-param-reassign:["error", { "props": false }] */
55 | const fakeThrottle = func => {
56 | func.cancel = cancelSpy;
57 | return func;
58 | };
59 |
60 | const Container = withMousePosition(fakeThrottle)(Div);
61 | const wrapper = mount();
62 |
63 | wrapper.unmount();
64 |
65 | expect(cancelSpy).toHaveBeenCalled();
66 | });
67 |
--------------------------------------------------------------------------------
/test/withOffsetToRoot.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 | import simulant from "simulant";
5 |
6 | import withOffsetToRoot from "../src/withOffsetToRoot";
7 |
8 | class Div extends React.Component {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
14 | test("append offsetToRoot after mounting", () => {
15 | const Container = withOffsetToRoot(f => f)(Div);
16 | const wrapper = mount();
17 |
18 | expect(wrapper.find(Div).props()).toInclude({
19 | offsetToRoot: { offsetTop: 0, offsetLeft: 0 }
20 | });
21 | });
22 |
23 | test("update offsetToRoot when the window is resized", () => {
24 | const cwrpSpy = expect.createSpy();
25 |
26 | class Foo extends React.Component {
27 | componentWillReceiveProps = cwrpSpy;
28 | render = () => null;
29 | }
30 |
31 | const Container = withOffsetToRoot(f => f)(Foo);
32 |
33 | mount();
34 |
35 | // invoke in cdm
36 | expect(cwrpSpy.calls.length).toEqual(1);
37 |
38 | // invoke when resizing
39 | simulant.fire(window, "resize");
40 | simulant.fire(window, "resize");
41 |
42 | expect(cwrpSpy.calls.length).toEqual(3);
43 | });
44 |
--------------------------------------------------------------------------------
/test/withSize.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 |
5 | import withSize from "../src/withSize";
6 |
7 | class Div extends React.Component {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
13 | test("append DOMSize after mounting", () => {
14 | const Container = withSize(f => f)(Div);
15 | const wrapper = mount();
16 |
17 | expect(wrapper.find(Div).props()).toIncludeKey("DOMSize");
18 | });
19 |
20 | test("invoke the cancel function of the provided throttle when unmount", () => {
21 | const cancelSpy = expect.createSpy();
22 |
23 | /* eslint no-param-reassign:["error", { "props": false }] */
24 | const fakeThrottle = func => {
25 | func.cancel = cancelSpy;
26 | return func;
27 | };
28 |
29 | const Container = withSize(fakeThrottle)(Div);
30 | const wrapper = mount();
31 |
32 | wrapper.unmount();
33 |
34 | expect(cancelSpy).toHaveBeenCalled();
35 | });
36 |
--------------------------------------------------------------------------------
/test/withWindowSize.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import expect from "expect";
4 | import simulant from "simulant";
5 |
6 | import withWindowSize, { pickedProps } from "../src/withWindowSize";
7 |
8 | class Div extends React.Component {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
14 | test("append withWindowSize after mounting", () => {
15 | const Container = withWindowSize(f => f)(Div);
16 | const wrapper = mount();
17 |
18 | expect(wrapper.find(Div).props().windowSize).toIncludeKeys(pickedProps);
19 | });
20 |
21 | test("update windowSize when the window is resized", () => {
22 | const cwrpSpy = expect.createSpy();
23 |
24 | class Foo extends React.Component {
25 | componentWillReceiveProps = cwrpSpy;
26 | render = () => null;
27 | }
28 |
29 | const Container = withWindowSize(f => f)(Foo);
30 |
31 | const wrapper = mount();
32 |
33 | // invoke in cdm
34 | expect(cwrpSpy.calls.length).toEqual(1);
35 |
36 | simulant.fire(window, "resize");
37 | simulant.fire(window, "resize");
38 |
39 | expect(cwrpSpy.calls.length).toEqual(3);
40 |
41 | wrapper.unmount();
42 |
43 | simulant.fire(window, "resize");
44 | expect(cwrpSpy.calls.length).toEqual(3);
45 | });
46 |
--------------------------------------------------------------------------------