Update Image props and implementation

This commit is contained in:
Nicolas Gallagher
2015-09-07 09:42:23 -07:00
parent cfdbd351f0
commit 90e015112a
6 changed files with 323 additions and 119 deletions
@@ -0,0 +1,4 @@
import View from '../View'
export default {
...(View.stylePropTypes)
}
+215
View File
@@ -0,0 +1,215 @@
/* global window */
import { pickProps } from '../../modules/filterObjectProps'
import CoreComponent from '../CoreComponent'
import ImageStylePropTypes from './ImageStylePropTypes'
import React, { PropTypes } from 'react'
import View from '../View'
const STATUS_ERRORED = 'ERRORED'
const STATUS_LOADED = 'LOADED'
const STATUS_LOADING = 'LOADING'
const STATUS_PENDING = 'PENDING'
const STATUS_IDLE = 'IDLE'
const styles = {
initial: {
alignSelf: 'flex-start',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundSize: 'cover'
},
img: {
borderWidth: 0,
height: 'auto',
maxHeight: '100%',
maxWidth: '100%',
opacity: 0
},
children: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0
},
resizeMode: {
clip: {
backgroundSize: 'auto'
},
contain: {
backgroundSize: 'contain'
},
cover: {
backgroundSize: 'cover'
},
stretch: {
backgroundSize: '100% 100%'
}
}
}
class Image extends React.Component {
constructor(props, context) {
super(props, context)
// state
this.state = { status: props.source.uri ? STATUS_PENDING : STATUS_IDLE }
// autobinding
this._onError = this._onError.bind(this)
this._onLoad = this._onLoad.bind(this)
}
static propTypes = {
accessibilityLabel: PropTypes.string,
children: PropTypes.any,
defaultSource: PropTypes.object,
onError: PropTypes.func,
onLoad: PropTypes.func,
onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func,
resizeMode: PropTypes.oneOf(['clip', 'contain', 'cover', 'stretch']),
source: PropTypes.object,
style: PropTypes.shape(ImageStylePropTypes),
testID: CoreComponent.propTypes.testID
}
static stylePropTypes = ImageStylePropTypes
static defaultProps = {
defaultSource: {},
resizeMode: 'cover',
source: {},
style: styles.initial
}
_cancelEvent(event) {
event.preventDefault()
event.stopPropagation()
}
_createImageLoader() {
const { source } = this.props
this._destroyImageLoader()
this.image = new window.Image()
this.image.onerror = this._onError
this.image.onload = this._onLoad
this.image.src = source.uri
this._onLoadStart()
}
_destroyImageLoader() {
if (this.image) {
this.image.onload = null
this.image.onerror = null
this.image = null
}
}
_onError(e) {
const { onError } = this.props
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_ERRORED })
if (onError) onError(event)
this._onLoadEnd()
}
_onLoad(e) {
const { onLoad } = this.props
const event = { nativeEvent: e }
this._destroyImageLoader()
this.setState({ status: STATUS_LOADED })
if (onLoad) onLoad(event)
this._onLoadEnd()
}
_onLoadEnd() {
const { onLoadEnd } = this.props
if (onLoadEnd) onLoadEnd()
}
_onLoadStart() {
const { onLoadStart } = this.props
this.setState({ status: STATUS_LOADING })
if (onLoadStart) onLoadStart()
}
componentDidMount() {
if (this.state.status === STATUS_PENDING) {
this._createImageLoader()
}
}
componentDidUpdate() {
if (this.state.status === STATUS_PENDING && !this.image) {
this._createImageLoader()
}
}
componentWillReceiveProps(nextProps) {
if (this.props.source.uri !== nextProps.source.uri) {
this.setState({
status: nextProps.source.uri ? STATUS_PENDING : STATUS_IDLE
})
}
}
componentWillUnmount() {
this._destroyImageLoader()
}
render() {
const {
accessibilityLabel,
children,
defaultSource,
resizeMode,
source,
style,
testID
} = this.props
const isLoaded = this.state.status === STATUS_LOADED
const defaultImage = defaultSource.uri || null
const displayImage = !isLoaded ? defaultImage : source.uri
const resolvedStyle = pickProps(style, Object.keys(ImageStylePropTypes))
const backgroundImage = displayImage ? `url(${displayImage})` : null
/**
* Image is a non-stretching View. The image is displayed as a background
* image to support `resizeMode`. The HTML image is hidden but used to
* provide the correct responsive image dimensions, and to support the
* image context menu. Child content is rendered into an element absolutely
* positioned over the image.
*/
return (
<View
accessibilityLabel={accessibilityLabel}
aria-role='img'
className={'Image'}
component='div'
style={{
...(styles.initial),
...resolvedStyle,
...(backgroundImage && { backgroundImage }),
...(styles.resizeMode[resizeMode])
}}
testID={testID}
>
<img
src={displayImage}
style={styles.img}
/>
{children ? (
<View children={children} pointerEvents='box-none' style={styles.children} />
) : null}
</View>
)
}
}
export default Image
+104
View File
@@ -0,0 +1,104 @@
import assert from 'assert'
import React from 'react/addons'
import Image from '.'
import View from '../View'
const ReactTestUtils = React.addons.TestUtils
function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
shallowRenderer.render(component, context)
return shallowRenderer.getRenderOutput()
}
function render(component, node) {
return node ? React.render(component, node) : ReactTestUtils.renderIntoDocument(component)
}
function getImageDOM(props) {
const result = ReactTestUtils.renderIntoDocument(<Image {...props} />)
return React.findDOMNode(result)
}
suite('Image', () => {
test('defaults', () => {
const result = shallowRender(<Image />)
assert.equal(result.type, View)
})
test('prop "accessibilityLabel"', () => {
const accessibilityLabel = 'accessibilityLabel'
const element = getImageDOM()
const elementHasLabel = getImageDOM({ accessibilityLabel })
assert.equal(element.getAttribute('aria-label'), null)
assert.equal(elementHasLabel.getAttribute('aria-label'), accessibilityLabel)
})
test.skip('prop "children"', () => { })
test('prop "defaultSource"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' }
const elementHasdefaultSource = getImageDOM({ defaultSource })
assert.equal(elementHasdefaultSource.style.backgroundImage, `url(${defaultSource.uri})`)
})
test('prop "onError"', (done) => {
function onError(e) {
assert.equal(e.nativeEvent.type, 'error')
done()
}
render(<Image
onError={onError}
source={{ uri: 'https://google.com/favicon.icox' }}
/>)
})
test('prop "onLoad"', (done) => {
function onLoad(e) {
assert.equal(e.nativeEvent.type, 'load')
done()
}
render(<Image
onLoad={onLoad}
source={{ uri: 'https://google.com/favicon.ico' }}
/>)
})
test.skip('prop "onLoadEnd"', () => { })
test.skip('prop "onLoadStart"', () => { })
test.skip('prop "resizeMode"', () => { })
test.skip('prop "source"', () => { })
test('prop "style"', () => {
const initial = shallowRender(<Image />)
assert.deepEqual(
initial.props.style,
Image.defaultProps.style
)
const unsupported = shallowRender(<Image style={{ unsupported: 'true' }} />)
assert.deepEqual(
unsupported.props.style.unsupported,
null,
'unsupported "style" properties must not be transferred'
)
})
test('prop "testID"', () => {
const testID = 'Example.image'
const elementHasTestID = getImageDOM({ testID })
assert.equal(
elementHasTestID.getAttribute('data-testid'),
testID
)
})
})
-17
View File
@@ -1,17 +0,0 @@
import { StylePropTypes } from '../react-native-web-style'
import { PropTypes } from 'react'
export default {
...StylePropTypes.BorderThemePropTypes,
...StylePropTypes.LayoutPropTypes,
backgroundColor: PropTypes.string,
boxShadow: PropTypes.string,
opacity: PropTypes.number,
transform: PropTypes.string
}
export const ImageDefaultStyle = {
backgroundColor: 'lightGray',
borderWidth: 0,
maxWidth: '100%'
}
-38
View File
@@ -1,38 +0,0 @@
import { pickProps } from '../filterObjectProps'
import { WebStyleComponent } from '../react-native-web-style'
import ImageStylePropTypes, { ImageDefaultStyle } from './ImageStylePropTypes'
import React, { PropTypes } from 'react'
class Image extends React.Component {
static propTypes = {
accessibilityLabel: PropTypes.string,
className: PropTypes.string,
source: PropTypes.object,
style: PropTypes.shape(ImageStylePropTypes)
}
static defaultProps = {
className: '',
source: 'data:image/gif;base64,' +
'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
}
render() {
const { accessibilityLabel, className, source, style, ...other } = this.props
const filteredStyle = pickProps(style, Object.keys(ImageStylePropTypes))
const mergedStyle = { ...ImageDefaultStyle, ...filteredStyle }
return (
<WebStyleComponent
{...other}
alt={accessibilityLabel}
className={`Image ${className}`}
component='img'
src={source.uri}
style={mergedStyle}
/>
)
}
}
export default Image
-64
View File
@@ -1,64 +0,0 @@
import assert from 'assert'
import React from 'react/addons'
import { ImageDefaultStyle } from './ImageStylePropTypes'
import Image from '.'
const ReactTestUtils = React.addons.TestUtils
function shallowRender(component, context = {}) {
const shallowRenderer = React.addons.TestUtils.createRenderer()
shallowRenderer.render(component, context)
return shallowRenderer.getRenderOutput()
}
suite('Image', () => {
test('defaults', () => {
const result = ReactTestUtils.renderIntoDocument(<Image />)
const root = React.findDOMNode(result)
assert.equal((root.tagName).toLowerCase(), 'img')
})
test('prop "accessibilityLabel"', () => {
const label = 'accessibilityLabel'
const result = ReactTestUtils.renderIntoDocument(<Image accessibilityLabel={label} />)
const root = React.findDOMNode(result)
assert.equal(root.getAttribute('alt'), label)
})
test('prop "className"', () => {
const className = 'className'
const result = shallowRender(<Image className={className} />)
assert.ok(
(result.props.className).indexOf(className) > -1,
'"className" was not transferred'
)
})
test('prop "source"', () => {
const source = { uri: 'path-to-image' }
const result = ReactTestUtils.renderIntoDocument(<Image source={source} />)
const root = React.findDOMNode(result)
assert.equal(root.getAttribute('src'), source.uri)
})
test('prop "style"', () => {
const initial = shallowRender(<Image />)
assert.deepEqual(
initial.props.style,
ImageDefaultStyle,
'default "style" is incorrect'
)
const unsupported = shallowRender(<Image style={{ unsupported: 'true' }} />)
assert.deepEqual(
unsupported.props.style.unsupported,
null,
'unsupported "style" properties must not be transferred'
)
})
})