mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-06-03 18:52:07 +00:00
Update Image props and implementation
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import View from '../View'
|
||||
export default {
|
||||
...(View.stylePropTypes)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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%'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user