[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

View File

@@ -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

View File

@@ -4,6 +4,8 @@ TODO
## Props
[...ScrollView props](./ScrollView.md)
**children**: any
Content to display over the image.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -33,6 +33,7 @@ export default class App extends React.Component {
<Heading size='large'>Image</Heading>
<Image
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
accessibilityLabel='accessible image'
children={<Text>Inner content</Text>}
defaultSource={{
@@ -57,6 +58,7 @@ export default class App extends React.Component {
<Heading size='large'>Text</Heading>
<Text
onPress={(e) => { console.log('Text.onPress', e) }}
onLayout={(e) => { console.log(e.nativeEvent.layout) }}
testID={'Example.text'}
>
PRESS ME.

View File

@@ -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]

View File

@@ -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,

View File

@@ -14,6 +14,15 @@ suite('components/Text', () => {
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) => {
const text = mount(<Text onPress={onPress} />)
text.simulate('click')

View File

@@ -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: {

View File

@@ -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 {
<View
accessibilityLabel={accessibilityLabel}
onClick={this._handleClick}
onLayout={onLayout}
style={[ styles.initial, rootStyles ]}
testID={testID}
>

View File

@@ -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(<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"', () => {
const view = shallow(<View pointerEvents='box-only' />)
assert.ok(includes(view.prop('className'), '__style_pebo') === true)

View File

@@ -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

View File

@@ -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

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