From 5a04d07a35fe0a7d8db9f0ddc3efe27f06c6e682 Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Fri, 2 Mar 2018 23:31:25 +0100 Subject: [PATCH] [change] call 'onLayout' when elements are resized Uses ResizeObserver to monitor layout changes. Falls back to window.onresize (with initial firing). Fix #60 Close #848 --- .../src/modules/applyLayout/index.js | 79 +++++++++++++++---- website/guides/getting-started.md | 5 +- .../storybook/1-components/Text/TextScreen.js | 4 +- .../storybook/1-components/View/ViewScreen.js | 4 + .../View/examples/PropOnLayout.js | 76 ++++++++++++++++++ .../View/examples/PropPointerEvents.js | 4 +- 6 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 website/storybook/1-components/View/examples/PropOnLayout.js diff --git a/packages/react-native-web/src/modules/applyLayout/index.js b/packages/react-native-web/src/modules/applyLayout/index.js index ecfbd6697..f4085ecff 100644 --- a/packages/react-native-web/src/modules/applyLayout/index.js +++ b/packages/react-native-web/src/modules/applyLayout/index.js @@ -9,6 +9,7 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import debounce from 'debounce'; +import findNodeHandle from '../../exports/findNodeHandle'; const emptyObject = {}; const registry = {}; @@ -16,22 +17,68 @@ const registry = {}; let id = 1; const guid = () => `r-${id++}`; +let resizeObserver; if (canUseDOM) { - const triggerAll = () => { - Object.keys(registry).forEach(key => { - const instance = registry[key]; - instance._handleLayout(); + if (typeof window.ResizeObserver !== 'undefined') { + resizeObserver = new window.ResizeObserver(entries => { + entries.forEach(({ target }) => { + const instance = registry[target._onLayoutId]; + instance && instance._handleLayout(); + }); }); - }; + } else { + if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + console.warn( + 'onLayout relies on ResizeObserver which is not supported by your browser. ' + + 'Please include a polyfill, e.g., https://github.com/que-etc/resize-observer-polyfill. ' + + 'Falling back to window.onresize.' + ); + } - window.addEventListener('resize', debounce(triggerAll, 16), false); + const triggerAll = () => { + Object.keys(registry).forEach(key => { + const instance = registry[key]; + instance._handleLayout(); + }); + }; + + window.addEventListener('resize', debounce(triggerAll, 16), false); + } } +const observe = instance => { + const id = guid(); + registry[id] = instance; + + if (resizeObserver) { + const node = findNodeHandle(instance); + node._onLayoutId = id; + resizeObserver.observe(node); + } else { + const id = guid(); + instance._onLayoutId = id; + instance._handleLayout(); + } +}; + +const unobserve = instance => { + delete registry[instance._onLayoutId]; + if (resizeObserver) { + const node = findNodeHandle(instance); + delete node._onLayoutId; + resizeObserver.unobserve(node); + } else { + delete instance._onLayoutId; + } +}; + const safeOverride = (original, next) => { if (original) { return function prototypeOverride() { - original.call(this); - next.call(this); + /* eslint-disable prefer-rest-params */ + original.call(this, arguments); + next.call(this, arguments); + /* eslint-enable prefer-rest-params */ }; } return next; @@ -47,16 +94,20 @@ const applyLayout = Component => { function componentDidMount() { this._layoutState = emptyObject; this._isMounted = true; - this._onLayoutId = guid(); - registry[this._onLayoutId] = this; - this._handleLayout(); + observe(this); } ); Component.prototype.componentDidUpdate = safeOverride( componentDidUpdate, - function componentDidUpdate() { - this._handleLayout(); + function componentDidUpdate(prevProps) { + if (this.props.onLayout && !prevProps.onLayout) { + observe(this); + } else if (!this.props.onLayout && prevProps.onLayout) { + unobserve(this); + } else if (!resizeObserver) { + this._handleLayout(); + } } ); @@ -64,7 +115,7 @@ const applyLayout = Component => { componentWillUnmount, function componentWillUnmount() { this._isMounted = false; - delete registry[this._onLayoutId]; + unobserve(this); } ); diff --git a/website/guides/getting-started.md b/website/guides/getting-started.md index ba6050703..d2f1e2475 100644 --- a/website/guides/getting-started.md +++ b/website/guides/getting-started.md @@ -2,8 +2,9 @@ This guide will help you to use and test React Native for Web once it has been installed. -Your application may need to polyfill `Promise`, `Object.assign`, and -`Array.from` as necessary for your desired browser support. +Your application may need to polyfill `Promise`, `Object.assign`, `Array.from`, +and [`ResizeObserver`](https://github.com/que-etc/resize-observer-polyfill) as +necessary for your desired browser support. ## Adding to a new web app diff --git a/website/storybook/1-components/Text/TextScreen.js b/website/storybook/1-components/Text/TextScreen.js index 7e275bd80..509e13bc4 100644 --- a/website/storybook/1-components/Text/TextScreen.js +++ b/website/storybook/1-components/Text/TextScreen.js @@ -132,13 +132,13 @@ const TextScreen = () => ( Invoked on mount and layout changes with{' '} {'{ nativeEvent: { layout: { x, y, width, height } } }'}, where{' '} x and y are the offsets from the parent node. - } + ]} /> ( x and y are the offsets from the parent node. } + example={{ + render: () => + }} /> { + if (this._ref) { + this._ref.setNativeProps({ style: this._layout }); + this._layout = this._layout.width === '100%' ? l2 : l1; + } + }, 2000); + } + + componentWillUnmount() { + clearInterval(this._interval); + } + + render() { + const { x, y, width, height } = this.state.layoutInfo; + return ( + + + x: {x} + y: {y} + width: {width} + height: {height} + + + + + + + + ); + } + + _handleLayout = ({ nativeEvent }) => { + this.setState(() => ({ layoutInfo: nativeEvent.layout })); + }; + + _setRef = component => { + this._ref = component; + }; +} + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row' + }, + container: { + height: 50 + }, + left: { + width: 100 + }, + right: { + flex: 1 + }, + box: { + backgroundColor: '#eee', + flex: 1 + } +}); diff --git a/website/storybook/1-components/View/examples/PropPointerEvents.js b/website/storybook/1-components/View/examples/PropPointerEvents.js index 3eb81e76b..70b921c62 100644 --- a/website/storybook/1-components/View/examples/PropPointerEvents.js +++ b/website/storybook/1-components/View/examples/PropPointerEvents.js @@ -18,7 +18,7 @@ const Box = ({ pointerEvents }) => ( ); -const ViewStyleExample = () => ( +const ViewPointerEventsExample = () => ( @@ -43,4 +43,4 @@ const styles = StyleSheet.create({ borderStyle: 'solid' } }); -export default ViewStyleExample; +export default ViewPointerEventsExample;