From 597fcc65e889fd8bb7a0d31d6c868243d2f6b285 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sun, 10 Jul 2016 22:15:51 -0700 Subject: [PATCH] [add] initial 'onLayout' support Add initial support for 'onLayout' when components mount and update. Ref #60 --- docs/components/Image.md | 5 ++- docs/components/ListView.md | 2 + docs/components/ScrollView.md | 2 - docs/components/Text.md | 5 +++ docs/components/TextInput.md | 13 +------ docs/components/TouchableWithoutFeedback.md | 9 +++-- docs/components/View.md | 3 +- examples/components/App.js | 2 + src/apis/UIManager/index.js | 2 +- src/components/Image/index.js | 5 +++ src/components/Text/__tests__/index-test.js | 9 +++++ src/components/Text/index.js | 19 +++++---- src/components/TextInput/index.js | 6 +-- src/components/View/__tests__/index-test.js | 11 +++++- src/components/View/index.js | 5 ++- src/modules/NativeMethodsMixin/index.js | 8 ++-- src/modules/applyLayout/index.js | 43 +++++++++++++++++++++ 17 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 src/modules/applyLayout/index.js diff --git a/docs/components/Image.md b/docs/components/Image.md index c04c8495..6dc9f1be 100644 --- a/docs/components/Image.md +++ b/docs/components/Image.md @@ -31,7 +31,8 @@ Invoked on load error with `{nativeEvent: {error}}`. **onLayout**: function -TODO +Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width, +height } } }`, where `x` and `y` are the offsets from the parent node. **onLoad**: function @@ -57,7 +58,7 @@ could be an http address or a base64 encoded image. **style**: style -+ ...[View#style](View.md) ++ ...[View#style](./View.md) + `resizeMode` **testID**: string diff --git a/docs/components/ListView.md b/docs/components/ListView.md index cc8308c5..126ce84a 100644 --- a/docs/components/ListView.md +++ b/docs/components/ListView.md @@ -4,6 +4,8 @@ TODO ## Props +[...ScrollView props](./ScrollView.md) + **children**: any Content to display over the image. diff --git a/docs/components/ScrollView.md b/docs/components/ScrollView.md index 9d2a18e3..9a94c45c 100644 --- a/docs/components/ScrollView.md +++ b/docs/components/ScrollView.md @@ -29,8 +29,6 @@ Determines whether the keyboard gets dismissed in response to a scroll drag. **onContentSizeChange**: function -TODO - Called when scrollable content view of the `ScrollView` changes. It's implemented using the `onLayout` handler attached to the content container which this `ScrollView` renders. diff --git a/docs/components/Text.md b/docs/components/Text.md index acc4c23a..cdeea937 100644 --- a/docs/components/Text.md +++ b/docs/components/Text.md @@ -45,6 +45,11 @@ Child content. Truncates the text with an ellipsis after this many lines. Currently only supports `1`. +**onLayout**: function + +Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width, +height } } }`, where `x` and `y` are the offsets from the parent node. + **onPress**: function This function is called on press. diff --git a/docs/components/TextInput.md b/docs/components/TextInput.md index c15464e0..f1ffdd76 100644 --- a/docs/components/TextInput.md +++ b/docs/components/TextInput.md @@ -14,16 +14,11 @@ Unsupported React Native props: `enablesReturnKeyAutomatically` (ios), `returnKeyType` (ios), `selectionState` (ios), -`textAlign` (android), -`textAlignVertical` (android), `underlineColorAndroid` (android) ## Props -(web) **accessibilityLabel**: string - -Defines the text label available to assistive technologies upon interaction -with the element. (This is implemented using `aria-label`.) +[...View props](./View.md) (web) **autoComplete**: bool = false @@ -92,10 +87,6 @@ as an argument to the callback handler. Callback that is called when the text input is focused. -**onLayout**: function - -TODO - (web) **onSelectionChange**: function Callback that is called when the text input's selection changes. The following @@ -132,7 +123,7 @@ If `true`, all text will automatically be selected on focus. **style**: style -+ ...[Text#style](Text.md) ++ ...[Text#style](./Text.md) + `outline` **testID**: string diff --git a/docs/components/TouchableWithoutFeedback.md b/docs/components/TouchableWithoutFeedback.md index 10f399a3..ec8bfdd9 100644 --- a/docs/components/TouchableWithoutFeedback.md +++ b/docs/components/TouchableWithoutFeedback.md @@ -9,6 +9,8 @@ several child components, wrap them in a View. ## Props +[...View props](./View.md) + **accessibilityLabel**: string Overrides the text that's read by the screen reader when the user interacts @@ -22,6 +24,8 @@ Allows assistive technologies to present and support interaction with the view When `false`, the view is hidden from screenreaders. +**children**: View + **delayLongPress**: number Delay in ms, from `onPressIn`, before `onLongPress` is called. @@ -47,9 +51,8 @@ always takes precedence if a touch hits two overlapping views. **onLayout**: function -Invoked on mount and layout changes with. - -`{nativeEvent: {layout: {x, y, width, height}}}` +Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width, +height } } }`, where `x` and `y` are the offsets from the parent node. **onLongPress**: function diff --git a/docs/components/View.md b/docs/components/View.md index 61990bc3..c5120fa2 100644 --- a/docs/components/View.md +++ b/docs/components/View.md @@ -48,7 +48,8 @@ implemented using `aria-hidden`.) **onLayout**: function -(TODO) +Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width, +height } } }`, where `x` and `y` are the offsets from the parent node. **onMoveShouldSetResponder**: function diff --git a/examples/components/App.js b/examples/components/App.js index 5b070778..07a512ae 100644 --- a/examples/components/App.js +++ b/examples/components/App.js @@ -33,6 +33,7 @@ export default class App extends React.Component { Image { console.log(e.nativeEvent.layout) }} accessibilityLabel='accessible image' children={Inner content} defaultSource={{ @@ -57,6 +58,7 @@ export default class App extends React.Component { Text { console.log('Text.onPress', e) }} + onLayout={(e) => { console.log(e.nativeEvent.layout) }} testID={'Example.text'} > PRESS ME. diff --git a/src/apis/UIManager/index.js b/src/apis/UIManager/index.js index 5f07e4bf..a35bc766 100644 --- a/src/apis/UIManager/index.js +++ b/src/apis/UIManager/index.js @@ -33,7 +33,7 @@ const UIManager = { _measureLayout(node, relativeTo, onSuccess) }, - updateView(node, props, component /* only needed to surpress React errors in __DEV__ */) { + updateView(node, props, component /* only needed to surpress React errors in development */) { for (const prop in props) { const value = props[prop] diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 7c801c8d..4600bf67 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -23,12 +23,15 @@ const ImageSourcePropType = PropTypes.oneOfType([ ]) class Image extends Component { + static displayName = 'Image' + static propTypes = { accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessible: createReactDOMComponent.propTypes.accessible, children: PropTypes.any, defaultSource: ImageSourcePropType, onError: PropTypes.func, + onLayout: PropTypes.func, onLoad: PropTypes.func, onLoadEnd: PropTypes.func, onLoadStart: PropTypes.func, @@ -82,6 +85,7 @@ class Image extends Component { accessible, children, defaultSource, + onLayout, source, testID } = this.props @@ -107,6 +111,7 @@ class Image extends Component { accessibilityLabel={accessibilityLabel} accessibilityRole='img' accessible={accessible} + onLayout={onLayout} ref='root' style={[ styles.initial, diff --git a/src/components/Text/__tests__/index-test.js b/src/components/Text/__tests__/index-test.js index b0247467..0eb1170d 100644 --- a/src/components/Text/__tests__/index-test.js +++ b/src/components/Text/__tests__/index-test.js @@ -14,6 +14,15 @@ suite('components/Text', () => { test('prop "numberOfLines"') + test('prop "onLayout"', (done) => { + mount() + function onLayout(e) { + const { layout } = e.nativeEvent + assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 }) + done() + } + }) + test('prop "onPress"', (done) => { const text = mount() text.simulate('click') diff --git a/src/components/Text/index.js b/src/components/Text/index.js index b9862a33..8359edb1 100644 --- a/src/components/Text/index.js +++ b/src/components/Text/index.js @@ -1,3 +1,4 @@ +import applyLayout from '../../modules/applyLayout' import applyNativeMethods from '../../modules/applyNativeMethods' import createReactDOMComponent from '../../modules/createReactDOMComponent' import { Component, PropTypes } from 'react' @@ -6,12 +7,15 @@ import StyleSheetPropType from '../../propTypes/StyleSheetPropType' import TextStylePropTypes from './TextStylePropTypes' class Text extends Component { + static displayName = 'Text' + static propTypes = { accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole, accessible: createReactDOMComponent.propTypes.accessible, children: PropTypes.any, numberOfLines: PropTypes.number, + onLayout: PropTypes.func, onPress: PropTypes.func, style: StyleSheetPropType(TextStylePropTypes), testID: createReactDOMComponent.propTypes.testID @@ -21,16 +25,11 @@ class Text extends Component { accessible: true }; - _onPress = (e) => { - if (this.props.onPress) this.props.onPress(e) - } - render() { const { numberOfLines, - /* eslint-disable no-unused-vars */ - onPress, - /* eslint-enable no-unused-vars */ + onLayout, // eslint-disable-line + onPress, // eslint-disable-line style, ...other } = this.props @@ -46,9 +45,13 @@ class Text extends Component { ] }) } + + _onPress = (e) => { + if (this.props.onPress) this.props.onPress(e) + } } -applyNativeMethods(Text) +applyLayout(applyNativeMethods(Text)) const styles = StyleSheet.create({ initial: { diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 10fcefbe..cae95bda 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -73,9 +73,7 @@ class TextInput extends Component { render() { const { - /* eslint-disable react/prop-types */ - accessibilityLabel, - /* eslint-enable react/prop-types */ + accessibilityLabel, // eslint-disable-line autoComplete, autoFocus, defaultValue, @@ -85,6 +83,7 @@ class TextInput extends Component { maxNumberOfLines, multiline, numberOfLines, + onLayout, onSelectionChange, placeholder, placeholderTextColor, @@ -171,6 +170,7 @@ class TextInput extends Component { diff --git a/src/components/View/__tests__/index-test.js b/src/components/View/__tests__/index-test.js index 37a44465..cc3dd18e 100644 --- a/src/components/View/__tests__/index-test.js +++ b/src/components/View/__tests__/index-test.js @@ -3,8 +3,8 @@ import assert from 'assert' import includes from 'lodash/includes' import React from 'react' -import { shallow } from 'enzyme' import View from '../' +import { mount, shallow } from 'enzyme' suite('components/View', () => { test('prop "children"', () => { @@ -13,6 +13,15 @@ suite('components/View', () => { assert.equal(view.prop('children'), children) }) + test('prop "onLayout"', (done) => { + mount() + function onLayout(e) { + const { layout } = e.nativeEvent + assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 }) + done() + } + }) + test('prop "pointerEvents"', () => { const view = shallow() assert.ok(includes(view.prop('className'), '__style_pebo') === true) diff --git a/src/components/View/index.js b/src/components/View/index.js index 4d36c7da..38adb1ea 100644 --- a/src/components/View/index.js +++ b/src/components/View/index.js @@ -1,3 +1,4 @@ +import applyLayout from '../../modules/applyLayout' import applyNativeMethods from '../../modules/applyNativeMethods' import createReactDOMComponent from '../../modules/createReactDOMComponent' import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType' @@ -8,6 +9,8 @@ import StyleSheetPropType from '../../propTypes/StyleSheetPropType' import ViewStylePropTypes from './ViewStylePropTypes' class View extends Component { + static displayName = 'View' + static propTypes = { accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion, @@ -105,7 +108,7 @@ class View extends Component { } } -applyNativeMethods(View) +applyLayout(applyNativeMethods(View)) const styles = StyleSheet.create({ // https://github.com/facebook/css-layout#default-values diff --git a/src/modules/NativeMethodsMixin/index.js b/src/modules/NativeMethodsMixin/index.js index 790bf818..d1a573aa 100644 --- a/src/modules/NativeMethodsMixin/index.js +++ b/src/modules/NativeMethodsMixin/index.js @@ -113,11 +113,11 @@ const NativeMethodsMixin = { * In the future, we should cleanup callbacks by cancelling them instead of * using this. */ -const mountSafeCallback = (context: Component, callback: ?Function) => () => { - if (!callback || (context.isMounted && !context.isMounted())) { - return +const mountSafeCallback = (context: Component, callback: ?Function) => (...args) => { + if (!callback) { + return undefined } - return callback.apply(context, arguments) + return callback.apply(context, args) } module.exports = NativeMethodsMixin diff --git a/src/modules/applyLayout/index.js b/src/modules/applyLayout/index.js new file mode 100644 index 00000000..7fa3c7d9 --- /dev/null +++ b/src/modules/applyLayout/index.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * All rights reserved. + * + * @flow + */ + +import emptyFunction from 'fbjs/lib/emptyFunction' + +const applyLayout = (Component) => { + const componentDidMount = Component.prototype.componentDidMount || emptyFunction + const componentDidUpdate = Component.prototype.componentDidUpdate || emptyFunction + + Component.prototype.componentDidMount = function () { + componentDidMount() + this._layoutState = {} + this._handleLayout() + } + + Component.prototype.componentDidUpdate = function () { + componentDidUpdate() + this._handleLayout() + } + + Component.prototype._handleLayout = function () { + const layout = this._layoutState + const { onLayout } = this.props + + if (onLayout) { + this.measure((x, y, width, height) => { + if (layout.x !== x || layout.y !== y || layout.width !== width || layout.height !== height) { + const nextLayout = { x, y, width, height } + const nativeEvent = { layout: nextLayout } + onLayout({ nativeEvent }) + this._layoutState = nextLayout + } + }) + } + } + return Component +} + +module.exports = applyLayout