├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src └── ResizeObserver.js └── typings └── react-resize-observer.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "shippedProposals": true 5 | }], 6 | "@babel/preset-react", 7 | "@babel/preset-flow" 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-proposal-class-properties" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # EditorConfig: http://EditorConfig.org 3 | # 4 | # This files specifies some basic editor conventions for the files in this 5 | # project. Many editors support this standard, you simply need to find a plugin 6 | # for your favorite! 7 | # 8 | # For a full list of possible values consult the reference. 9 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 10 | # 11 | 12 | # Stop searching for other .editorconfig files above this folder. 13 | root = true 14 | 15 | # Pick some sane defaults for all files. 16 | [*] 17 | 18 | # UNIX line-endings are preferred. 19 | # http://adaptivepatchwork.com/2012/03/01/mind-the-end-of-your-line/ 20 | end_of_line = lf 21 | 22 | # No reason in these modern times to use anything other than UTF-8. 23 | charset = utf-8 24 | 25 | # Ensure that there's no bogus whitespace in the file. 26 | trim_trailing_whitespace = true 27 | 28 | # A little esoteric, but it's kind of a standard now. 29 | # http://stackoverflow.com/questions/729692/why-should-files-end-with-a-newline 30 | insert_final_newline = true 31 | 32 | # Pragmatism today. 33 | # http://programmers.stackexchange.com/questions/57 34 | indent_style = 2 35 | 36 | # Personal preference here. Smaller indent size means you can fit more on a line 37 | # which can be nice when there are lines with several indentations. 38 | indent_size = 2 39 | 40 | # Prefer a more conservative default line length – this allows editors with 41 | # sidebars, minimaps, etc. to show at least two documents side-by-side. 42 | # Hard wrapping by default for code is useful since many editors don't support 43 | # an elegant soft wrap; however, soft wrap is fine for things where text just 44 | # flows normally, like Markdown documents or git commit messages. Hard wrap 45 | # is also easier for line-based diffing tools to consume. 46 | # See: http://tex.stackexchange.com/questions/54140 47 | max_line_length = 80 48 | 49 | # Markdown uses trailing spaces to create line breaks. 50 | [*.md] 51 | trim_trailing_whitespace = false 52 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /flow-typed 2 | /*.js 3 | match/**/* 4 | internal/**/* 5 | test/*.js 6 | lib/**/* 7 | node_modules 8 | /coverage 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - metalab 3 | overrides: 4 | - 5 | files: "*.spec.js" 6 | env: 7 | jest: true 8 | env: 9 | browser: true 10 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*\.flow 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | all=error 9 | 10 | [options] 11 | emoji=true 12 | include_warnings=true 13 | suppress_comment=\\(.\\|\n\\)*\\$ExpectError 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | *.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | parser: flow 2 | semi: true 3 | singleQuote: true 4 | trailingComma: all 5 | bracketSpacing: false 6 | arrowParens: always 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | matrix: 4 | include: 5 | - node_js: '8' 6 | - node_js: '10' 7 | 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Izaak Schroeder 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-resize-observer 2 | 3 | Component for giving you `onResize`. 4 | 5 | ![build status](http://img.shields.io/travis/metalabdesign/react-resize-observer/master.svg?style=flat) 6 | ![coverage](http://img.shields.io/coveralls/metalabdesign/react-resize-observer/master.svg?style=flat) 7 | ![license](http://img.shields.io/npm/l/react-resize-observer.svg?style=flat) 8 | ![version](http://img.shields.io/npm/v/react-resize-observer.svg?style=flat) 9 | ![downloads](http://img.shields.io/npm/dm/react-resize-observer.svg?style=flat) 10 | 11 | ## Overview 12 | 13 | Primarily based on [this work] by Marc J. Schmidt. 14 | 15 | ## Usage 16 | 17 | ``` 18 | npm install --save react react-dom react-resize-observer 19 | ``` 20 | 21 | Add `ResizeObserver` to the element whose size or position you want to measure. The only requirement is that your component must _not_ have a `position` of `static` (see [Caveats](#caveats) section. 22 | 23 | ```jsx 24 | import ResizeObserver from 'react-resize-observer'; 25 | 26 | const MyComponent = () => ( 27 |
28 | Hello World 29 | { 31 | console.log('Resized. New bounds:', rect.width, 'x', rect.height); 32 | }} 33 | onPosition={(rect) => { 34 | console.log('Moved. New position:', rect.left, 'x', rect.top); 35 | }} 36 | /> 37 |
38 | ); 39 | ``` 40 | 41 | ## Component Props 42 | 43 | ### `onResize`: function 44 | 45 | *optional* 46 | 47 | Called with a single [`DOMRect`] argument when a size change is detected. 48 | 49 | ### `onPosition`: function 50 | 51 | *optional* 52 | 53 | Called with a single [`DOMRect`] argument when a position change is detected. 54 | 55 | ### `onReflow`: function 56 | 57 | *optional* 58 | 59 | Called with a single [`DOMRect`] argument when either a position or size change is detected. 60 | 61 | ## Caveats 62 | 63 | #### Target Element Style 64 | 65 | `ResizeObserver` will detect changes in the size or position of the closest containing block (an element with a position other than `static`) - so use either `fixed`, `absolute`, or `relative` on the element you are measuring. 66 | 67 | The mechanism used to detect element size changes relies on the behavior of nested, absolutely positioned elements and their ability to trigger scroll events on their parent element. This is the reason this library is implemented as a rendered child element, and not as component enhancer. 68 | 69 | #### Position Detection 70 | 71 | The `onPosition` (an `onReflow`) callbacks will detect when the measured element's position in the viewport changes, but only when the change is caused by a scroll event of the window or an ancestor element with `overflow: scroll`. Position changes caused by other factors (i.e. `transform`, `margin`, `top`/`left` etc.) will not be immediately detected - although these changes will be observed and returned the next time a scroll event is captured. 72 | 73 | If absolutely you need to capture position changes caused by style updates, calling `document.body.dispatchEvent(new UIEvent('scroll'))` will cause any mounted `ResizeObserver` instances to update. 74 | 75 | #### Callback Result 76 | 77 | This component returns raw `DOMRect` instances as the callback argument. `DOMRect` instances return `{}` when serialized to JSON (which will cause them to appear empty in Redux DevTools). `DOMRect` instances may crash the React Developer Tools extension if you try to inspect them as part of component state. 78 | 79 | If any of these quirks become an issue, the solution is to map the values you need onto a plain object: https://stackoverflow.com/questions/39417566. 80 | 81 | [this work]: https://github.com/marcj/css-element-queries/blob/master/src/ResizeSensor.js 82 | [`DOMRect`]: https://developer.mozilla.org/en-US/docs/Web/API/DOMRect 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resize-observer", 3 | "version": "1.1.1", 4 | "author": "Izaak Schroeder ", 5 | "license": "(MIT OR CC0-1.0)", 6 | "contributors": [ 7 | "Neal Granger " 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:metalabdesign/react-resize-observer.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "resize" 16 | ], 17 | "main": "lib/ResizeObserver.js", 18 | "types": "typings/react-resize-observer.d.ts", 19 | "files": [ 20 | "lib", 21 | "typings" 22 | ], 23 | "scripts": { 24 | "clean": "./node_modules/.bin/rimraf ./*.js ./*.map ./lib", 25 | "build": "./node_modules/.bin/babel --copy-files -s -d ./lib src && ./node_modules/.bin/flow-copy-source ./src ./lib", 26 | "prepublish": "npm run clean && npm run build", 27 | "test": "npm run lint && npm run flow", 28 | "flow": "./node_modules/.bin/flow .", 29 | "lint": "./node_modules/.bin/eslint ." 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.0.0-beta.37", 33 | "@babel/core": "^7.0.0-beta.37", 34 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.46", 35 | "@babel/preset-env": "^7.0.0-beta.37", 36 | "@babel/preset-flow": "^7.0.0-beta.37", 37 | "@babel/preset-react": "^7.0.0-beta.37", 38 | "@babel/register": "^7.0.0-beta.37", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-jest": "^22.4.3", 41 | "eslint": "^4.6.0", 42 | "eslint-config-metalab": "^9.0.1", 43 | "flow-bin": "^0.70.0", 44 | "flow-copy-source": "^1.3.0", 45 | "jest": "^22.4.3", 46 | "prettier": "^1.12.0", 47 | "react": "^16.3.2", 48 | "react-test-renderer": "^16.3.2", 49 | "rimraf": "^2.6.1" 50 | }, 51 | "peerDependencies": { 52 | "react": ">=0.14" 53 | }, 54 | "devEngines": { 55 | "node": "8", 56 | "npm": "5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ResizeObserver.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable complexity */ 3 | 4 | // ============================================================================= 5 | // Import modules. 6 | // ============================================================================= 7 | import React from 'react'; 8 | 9 | const style = { 10 | position: 'absolute', 11 | left: 0, 12 | top: 0, 13 | right: 0, 14 | bottom: 0, 15 | overflow: 'hidden', 16 | zIndex: -1, 17 | visibility: 'hidden', 18 | pointerEvents: 'none', 19 | }; 20 | const styleChild = { 21 | position: 'absolute', 22 | left: 0, 23 | top: 0, 24 | transition: '0s', 25 | }; 26 | 27 | function isAncestor(node: Node, ancestor: Node) { 28 | let current = node.parentNode; 29 | 30 | while (current) { 31 | if (current === ancestor) { 32 | return true; 33 | } 34 | 35 | current = current.parentNode; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | export type Props = { 42 | onResize?: (ClientRect) => mixed, 43 | onPosition?: (ClientRect) => mixed, 44 | onReflow?: (ClientRect) => mixed, 45 | }; 46 | 47 | class ResizeObserver extends React.Component { 48 | static displayName = 'ResizeObserver'; 49 | 50 | _expandRef: HTMLElement | null = null; 51 | _shrinkRef: HTMLElement | null = null; 52 | _node: HTMLElement | null = null; 53 | _lastWidth: ?number; 54 | _lastHeight: ?number; 55 | _lastRect: ClientRect; 56 | _hasResize: boolean = false; 57 | 58 | componentDidMount() { 59 | this._reflow(); 60 | 61 | window.addEventListener('scroll', this._handleScroll, true); 62 | 63 | if (this.props.onPosition || this.props.onReflow) { 64 | window.addEventListener('resize', this._reflow, true); 65 | this._hasResize = true; 66 | } 67 | } 68 | 69 | componentDidUpdate() { 70 | if ((this.props.onPosition || this.props.onReflow) && !this._hasResize) { 71 | window.addEventListener('resize', this._reflow, true); 72 | this._hasResize = true; 73 | } else if ( 74 | !(this.props.onPosition || this.props.onReflow) && 75 | this._hasResize 76 | ) { 77 | window.removeEventListener('resize', this._reflow, true); 78 | this._hasResize = false; 79 | } 80 | } 81 | 82 | componentWillUnmount() { 83 | window.removeEventListener('scroll', this._handleScroll, true); 84 | 85 | if (this._hasResize) { 86 | window.removeEventListener('resize', this._reflow, true); 87 | } 88 | } 89 | 90 | _handleScroll = (event: Event) => { 91 | if ( 92 | (this.props.onPosition || this.props.onReflow || this.props.onResize) && 93 | (this._globalScollTarget(event.target) || 94 | this._refScrollTarget(event.target) || 95 | this._ancestorScollTarget(event.target)) 96 | ) { 97 | this._reflow(); 98 | } 99 | }; 100 | 101 | _globalScollTarget = (target: EventTarget) => { 102 | return ( 103 | target instanceof Node && 104 | (this.props.onPosition || this.props.onReflow) && 105 | (target === document || 106 | target === document.documentElement || 107 | target === document.body) 108 | ); 109 | }; 110 | 111 | _refScrollTarget = (target: EventTarget) => { 112 | if ( 113 | target instanceof HTMLElement && 114 | (target === this._expandRef || target === this._shrinkRef) 115 | ) { 116 | const width = target.offsetWidth; 117 | const height = target.offsetHeight; 118 | 119 | if (width !== this._lastWidth || height !== this._lastHeight) { 120 | this._lastWidth = width; 121 | this._lastHeight = height; 122 | this._reset(this._expandRef); 123 | this._reset(this._shrinkRef); 124 | return true; 125 | } 126 | } 127 | return false; 128 | }; 129 | 130 | _ancestorScollTarget = (target: EventTarget) => { 131 | return ( 132 | target instanceof Node && 133 | (this.props.onPosition || this.props.onReflow) && 134 | this._node && 135 | isAncestor(this._node, target) 136 | ); 137 | }; 138 | 139 | _reflow = () => { 140 | if (!this._node || !(this._node.parentNode instanceof Element)) return; 141 | 142 | const rect = this._node.parentNode.getBoundingClientRect(); 143 | 144 | let sizeChanged = true; 145 | let positionChanged = true; 146 | 147 | if (this._lastRect) { 148 | sizeChanged = 149 | rect.width !== this._lastRect.width || 150 | rect.height !== this._lastRect.height; 151 | 152 | positionChanged = 153 | rect.top !== this._lastRect.top || rect.left !== this._lastRect.left; 154 | } 155 | 156 | this._lastRect = rect; 157 | 158 | if (sizeChanged && this.props.onResize) { 159 | this.props.onResize(rect); 160 | } 161 | 162 | if (positionChanged && this.props.onPosition) { 163 | this.props.onPosition(rect); 164 | } 165 | 166 | if ((sizeChanged || positionChanged) && this.props.onReflow) { 167 | this.props.onReflow(rect); 168 | } 169 | }; 170 | 171 | _reset(node: HTMLElement | null) { 172 | if (node) { 173 | node.scrollLeft = 100000; 174 | node.scrollTop = 100000; 175 | } 176 | } 177 | 178 | _handleRef = (node: HTMLElement | null) => { 179 | this._node = node; 180 | }; 181 | 182 | _handleExpandRef = (node: HTMLElement | null) => { 183 | this._reset(node); 184 | this._expandRef = node; 185 | }; 186 | 187 | _handleShrinkRef = (node: HTMLElement | null) => { 188 | this._reset(node); 189 | this._shrinkRef = node; 190 | }; 191 | 192 | render() { 193 | if (this.props.onResize || this.props.onReflow) { 194 | return ( 195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | ); 204 | } 205 | 206 | return