mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-06-04 02:56:42 +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