├── .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 | [![npm](https://img.shields.io/npm/v/react-dom-utils.svg)](https://www.npmjs.com/package/react-dom-utils) 4 | [![Travis](https://img.shields.io/travis/wuct/react-dom-utils/master.svg)](https://travis-ci.org/wuct/react-dom-utils) 5 | [![Codecov](https://img.shields.io/codecov/c/github/wuct/react-dom-utils/master.svg)](https://codecov.io/github/wuct/react-dom-utils) 6 | [![Code Climate](https://img.shields.io/codeclimate/github/wuct/react-dom-utils.svg)](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 |
55 |
56 |
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 | --------------------------------------------------------------------------------