[add] initial 'onLayout' support

Add initial support for 'onLayout' when components mount and update.

Ref #60
This commit is contained in:
Nicolas Gallagher
2016-07-10 22:15:51 -07:00
parent 5e1e0ec8e5
commit 597fcc65e8
17 changed files with 112 additions and 37 deletions
+3 -2
View File
@@ -31,7 +31,8 @@ Invoked on load error with `{nativeEvent: {error}}`.
**onLayout**: function **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 **onLoad**: function
@@ -57,7 +58,7 @@ could be an http address or a base64 encoded image.
**style**: style **style**: style
+ ...[View#style](View.md) + ...[View#style](./View.md)
+ `resizeMode` + `resizeMode`
**testID**: string **testID**: string
+2
View File
@@ -4,6 +4,8 @@ TODO
## Props ## Props
[...ScrollView props](./ScrollView.md)
**children**: any **children**: any
Content to display over the image. Content to display over the image.
-2
View File
@@ -29,8 +29,6 @@ Determines whether the keyboard gets dismissed in response to a scroll drag.
**onContentSizeChange**: function **onContentSizeChange**: function
TODO
Called when scrollable content view of the `ScrollView` changes. It's Called when scrollable content view of the `ScrollView` changes. It's
implemented using the `onLayout` handler attached to the content container implemented using the `onLayout` handler attached to the content container
which this `ScrollView` renders. which this `ScrollView` renders.
+5
View File
@@ -45,6 +45,11 @@ Child content.
Truncates the text with an ellipsis after this many lines. Currently only supports `1`. 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 **onPress**: function
This function is called on press. This function is called on press.
+2 -11
View File
@@ -14,16 +14,11 @@ Unsupported React Native props:
`enablesReturnKeyAutomatically` (ios), `enablesReturnKeyAutomatically` (ios),
`returnKeyType` (ios), `returnKeyType` (ios),
`selectionState` (ios), `selectionState` (ios),
`textAlign` (android),
`textAlignVertical` (android),
`underlineColorAndroid` (android) `underlineColorAndroid` (android)
## Props ## Props
(web) **accessibilityLabel**: string [...View props](./View.md)
Defines the text label available to assistive technologies upon interaction
with the element. (This is implemented using `aria-label`.)
(web) **autoComplete**: bool = false (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. Callback that is called when the text input is focused.
**onLayout**: function
TODO
(web) **onSelectionChange**: function (web) **onSelectionChange**: function
Callback that is called when the text input's selection changes. The following 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 **style**: style
+ ...[Text#style](Text.md) + ...[Text#style](./Text.md)
+ `outline` + `outline`
**testID**: string **testID**: string
+6 -3
View File
@@ -9,6 +9,8 @@ several child components, wrap them in a View.
## Props ## Props
[...View props](./View.md)
**accessibilityLabel**: string **accessibilityLabel**: string
Overrides the text that's read by the screen reader when the user interacts 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. When `false`, the view is hidden from screenreaders.
**children**: View
**delayLongPress**: number **delayLongPress**: number
Delay in ms, from `onPressIn`, before `onLongPress` is called. 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 **onLayout**: function
Invoked on mount and layout changes with. Invoked on mount and layout changes with `{ nativeEvent: { layout: { x, y, width,
height } } }`, where `x` and `y` are the offsets from the parent node.
`{nativeEvent: {layout: {x, y, width, height}}}`
**onLongPress**: function **onLongPress**: function
+2 -1
View File
@@ -48,7 +48,8 @@ implemented using `aria-hidden`.)
**onLayout**: function **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 **onMoveShouldSetResponder**: function
+2
View File
@@ -33,6 +33,7 @@ export default class App extends React.Component {
<Heading size='large'>Image</Heading> <Heading size='large'>Image</Heading>
<Image <Image
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
accessibilityLabel='accessible image' accessibilityLabel='accessible image'
children={<Text>Inner content</Text>} children={<Text>Inner content</Text>}
defaultSource={{ defaultSource={{
@@ -57,6 +58,7 @@ export default class App extends React.Component {
<Heading size='large'>Text</Heading> <Heading size='large'>Text</Heading>
<Text <Text
onPress={(e) => { console.log('Text.onPress', e) }} onPress={(e) => { console.log('Text.onPress', e) }}
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
testID={'Example.text'} testID={'Example.text'}
> >
PRESS ME. PRESS ME.
+1 -1
View File
@@ -33,7 +33,7 @@ const UIManager = {
_measureLayout(node, relativeTo, onSuccess) _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) { for (const prop in props) {
const value = props[prop] const value = props[prop]
+5
View File
@@ -23,12 +23,15 @@ const ImageSourcePropType = PropTypes.oneOfType([
]) ])
class Image extends Component { class Image extends Component {
static displayName = 'Image'
static propTypes = { static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessible: createReactDOMComponent.propTypes.accessible, accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any, children: PropTypes.any,
defaultSource: ImageSourcePropType, defaultSource: ImageSourcePropType,
onError: PropTypes.func, onError: PropTypes.func,
onLayout: PropTypes.func,
onLoad: PropTypes.func, onLoad: PropTypes.func,
onLoadEnd: PropTypes.func, onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func, onLoadStart: PropTypes.func,
@@ -82,6 +85,7 @@ class Image extends Component {
accessible, accessible,
children, children,
defaultSource, defaultSource,
onLayout,
source, source,
testID testID
} = this.props } = this.props
@@ -107,6 +111,7 @@ class Image extends Component {
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityRole='img' accessibilityRole='img'
accessible={accessible} accessible={accessible}
onLayout={onLayout}
ref='root' ref='root'
style={[ style={[
styles.initial, styles.initial,
@@ -14,6 +14,15 @@ suite('components/Text', () => {
test('prop "numberOfLines"') test('prop "numberOfLines"')
test('prop "onLayout"', (done) => {
mount(<Text onLayout={onLayout} />)
function onLayout(e) {
const { layout } = e.nativeEvent
assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 })
done()
}
})
test('prop "onPress"', (done) => { test('prop "onPress"', (done) => {
const text = mount(<Text onPress={onPress} />) const text = mount(<Text onPress={onPress} />)
text.simulate('click') text.simulate('click')
+11 -8
View File
@@ -1,3 +1,4 @@
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods' import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent' import createReactDOMComponent from '../../modules/createReactDOMComponent'
import { Component, PropTypes } from 'react' import { Component, PropTypes } from 'react'
@@ -6,12 +7,15 @@ import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import TextStylePropTypes from './TextStylePropTypes' import TextStylePropTypes from './TextStylePropTypes'
class Text extends Component { class Text extends Component {
static displayName = 'Text'
static propTypes = { static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole, accessibilityRole: createReactDOMComponent.propTypes.accessibilityRole,
accessible: createReactDOMComponent.propTypes.accessible, accessible: createReactDOMComponent.propTypes.accessible,
children: PropTypes.any, children: PropTypes.any,
numberOfLines: PropTypes.number, numberOfLines: PropTypes.number,
onLayout: PropTypes.func,
onPress: PropTypes.func, onPress: PropTypes.func,
style: StyleSheetPropType(TextStylePropTypes), style: StyleSheetPropType(TextStylePropTypes),
testID: createReactDOMComponent.propTypes.testID testID: createReactDOMComponent.propTypes.testID
@@ -21,16 +25,11 @@ class Text extends Component {
accessible: true accessible: true
}; };
_onPress = (e) => {
if (this.props.onPress) this.props.onPress(e)
}
render() { render() {
const { const {
numberOfLines, numberOfLines,
/* eslint-disable no-unused-vars */ onLayout, // eslint-disable-line
onPress, onPress, // eslint-disable-line
/* eslint-enable no-unused-vars */
style, style,
...other ...other
} = this.props } = 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({ const styles = StyleSheet.create({
initial: { initial: {
+3 -3
View File
@@ -73,9 +73,7 @@ class TextInput extends Component {
render() { render() {
const { const {
/* eslint-disable react/prop-types */ accessibilityLabel, // eslint-disable-line
accessibilityLabel,
/* eslint-enable react/prop-types */
autoComplete, autoComplete,
autoFocus, autoFocus,
defaultValue, defaultValue,
@@ -85,6 +83,7 @@ class TextInput extends Component {
maxNumberOfLines, maxNumberOfLines,
multiline, multiline,
numberOfLines, numberOfLines,
onLayout,
onSelectionChange, onSelectionChange,
placeholder, placeholder,
placeholderTextColor, placeholderTextColor,
@@ -171,6 +170,7 @@ class TextInput extends Component {
<View <View
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
onClick={this._handleClick} onClick={this._handleClick}
onLayout={onLayout}
style={[ styles.initial, rootStyles ]} style={[ styles.initial, rootStyles ]}
testID={testID} testID={testID}
> >
+10 -1
View File
@@ -3,8 +3,8 @@
import assert from 'assert' import assert from 'assert'
import includes from 'lodash/includes' import includes from 'lodash/includes'
import React from 'react' import React from 'react'
import { shallow } from 'enzyme'
import View from '../' import View from '../'
import { mount, shallow } from 'enzyme'
suite('components/View', () => { suite('components/View', () => {
test('prop "children"', () => { test('prop "children"', () => {
@@ -13,6 +13,15 @@ suite('components/View', () => {
assert.equal(view.prop('children'), children) assert.equal(view.prop('children'), children)
}) })
test('prop "onLayout"', (done) => {
mount(<View onLayout={onLayout} />)
function onLayout(e) {
const { layout } = e.nativeEvent
assert.deepEqual(layout, { x: 0, y: 0, width: 0, height: 0 })
done()
}
})
test('prop "pointerEvents"', () => { test('prop "pointerEvents"', () => {
const view = shallow(<View pointerEvents='box-only' />) const view = shallow(<View pointerEvents='box-only' />)
assert.ok(includes(view.prop('className'), '__style_pebo') === true) assert.ok(includes(view.prop('className'), '__style_pebo') === true)
+4 -1
View File
@@ -1,3 +1,4 @@
import applyLayout from '../../modules/applyLayout'
import applyNativeMethods from '../../modules/applyNativeMethods' import applyNativeMethods from '../../modules/applyNativeMethods'
import createReactDOMComponent from '../../modules/createReactDOMComponent' import createReactDOMComponent from '../../modules/createReactDOMComponent'
import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType' import EdgeInsetsPropType from '../../propTypes/EdgeInsetsPropType'
@@ -8,6 +9,8 @@ import StyleSheetPropType from '../../propTypes/StyleSheetPropType'
import ViewStylePropTypes from './ViewStylePropTypes' import ViewStylePropTypes from './ViewStylePropTypes'
class View extends Component { class View extends Component {
static displayName = 'View'
static propTypes = { static propTypes = {
accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel, accessibilityLabel: createReactDOMComponent.propTypes.accessibilityLabel,
accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion, accessibilityLiveRegion: createReactDOMComponent.propTypes.accessibilityLiveRegion,
@@ -105,7 +108,7 @@ class View extends Component {
} }
} }
applyNativeMethods(View) applyLayout(applyNativeMethods(View))
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// https://github.com/facebook/css-layout#default-values // https://github.com/facebook/css-layout#default-values
+4 -4
View File
@@ -113,11 +113,11 @@ const NativeMethodsMixin = {
* In the future, we should cleanup callbacks by cancelling them instead of * In the future, we should cleanup callbacks by cancelling them instead of
* using this. * using this.
*/ */
const mountSafeCallback = (context: Component, callback: ?Function) => () => { const mountSafeCallback = (context: Component, callback: ?Function) => (...args) => {
if (!callback || (context.isMounted && !context.isMounted())) { if (!callback) {
return return undefined
} }
return callback.apply(context, arguments) return callback.apply(context, args)
} }
module.exports = NativeMethodsMixin module.exports = NativeMethodsMixin
+43
View File
@@ -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