Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
'theme/index.js',
'theme/typography.js',
'utils/eventLogger.js',
'utils/vignette.js',
],
externalDependencies: [
'date-fns',
Expand Down
14 changes: 12 additions & 2 deletions config/styleguide.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"HideComponent",
"Icon",
"IconSprite",
"Image",
"ImagePreloader",
"Input",
"List",
"Select",
Expand All @@ -69,8 +71,7 @@
"Switch.Item",
"Switch.Wrapper",
"Timeago",
"VideoPlayIcon",
"Vignette"
"VideoPlayIcon"
]
},
{
Expand All @@ -90,6 +91,15 @@
"useLazyLoad"
]
},
{
"name": "Utils",
"sections": [
{
"name": "Vignette",
"content": "../source/utils/VIGNETTE.md"
}
]
},
{
"name": "Models",
"description": "Wrappers for immutable.js Records.",
Expand Down
20 changes: 20 additions & 0 deletions source/components/Image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Default:

```js
<Image
alt="test"
src="https://static.wikia.nocookie.net/2a6c845c-c6e9-472e-abed-24863817b795"
width="160"
height="100"
/>
```

Disable lazy loading

```js
<Image
alt="test"
src="https://static.wikia.nocookie.net/2a6c845c-c6e9-472e-abed-24863817b795"
disableLazy
/>
```
88 changes: 88 additions & 0 deletions source/components/Image/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Disabled lazyLoading 1`] = `
<Image
alt="test"
disableLazy={true}
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
/>
<noscript>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
/>
</noscript>
</Image>
`;

exports[`Image test 1`] = `
<Image
alt="test"
disableLazy={false}
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
>
<ImagePreloader
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
srcSet={null}
>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205/smart/width/5/height/5"
/>
</ImagePreloader>
<noscript>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
/>
</noscript>
</Image>
`;

exports[`Image test for non-vignette 1`] = `
<Image
alt="test"
disableLazy={false}
src="https://foo.jpg"
>
<img
alt="test"
className=""
src="https://foo.jpg"
/>
</Image>
`;

exports[`Updating an image src 1`] = `
<Image
alt="test"
disableLazy={false}
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
>
<ImagePreloader
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
srcSet={null}
>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205/smart/width/5/height/5"
/>
</ImagePreloader>
<noscript>
<img
alt="test"
className=""
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
/>
</noscript>
</Image>
`;
109 changes: 109 additions & 0 deletions source/components/Image/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

import ImagePreloader from '../ImagePreloader';
import { isVignetteUrl, vignette } from '../../utils/vignette';

const LAZY_IMAGE_SIZE = 5;

class Image extends React.Component {
static propTypes = {
alt: PropTypes.string.isRequired,
className: PropTypes.string,
disableLazy: PropTypes.bool,
src: PropTypes.string.isRequired,
srcSet: PropTypes.string,
};

static defaultProps = {
className: undefined,
disableLazy: false,
srcSet: undefined,
};

state = {
src: this.props.src,
isLimbo: false,
};

// When the src changes first replace the src with a temp image so it doesn't stall displaying
// the old image
// https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#when-to-use-derived-state
static getDerivedStateFromProps(props, state) {
// when only the src changes we are in "limbo" mode
return {
isLimbo: props.src !== state.src,
};
}

// after the component updates once we want to
componentDidUpdate() {
if (this.props.src !== this.state.src) {
// this is one of the rare cases to conditionally call setState after a component update
// this allows the images to be removed from the DOM properly
// eslint-disable-next-line react/no-did-update-set-state
this.setState(() => ({ src: this.props.src }));
}
}

/**
* Create a super low resolution image that will automatically be blurred in most browsers
*/
getLowResSrcFromVignette() {
return vignette(this.props.src).withSmart(LAZY_IMAGE_SIZE, LAZY_IMAGE_SIZE).get();
}

renderPlainImage() {
const { src, alt, className, disableLazy, ...rest } = this.props;

return <img className={classNames(className)} src={src} alt={alt} {...rest} />;
}

renderVignetteImage() {
const { src: _skip1, srcSet: _skip2, alt, className, disableLazy, ...rest } = this.props;

if (disableLazy) {
return this.renderPlainImage();
}

return (
<ImagePreloader src={this.state.src} srcSet={this.props.srcSet}>
{({ src, srcSet, state }) => {
// we will not test the functionality of ImagePreloader here
/* istanbul ignore next */
if (state !== ImagePreloader.STATE.PENDING) {
return <img className={classNames(className)} src={src} srcSet={srcSet} alt={alt} {...rest} />;
}

// if the image is loading, render low quality image
return <img className={classNames(className)} src={this.getLowResSrcFromVignette()} alt={alt} {...rest} />;
}}
</ImagePreloader>
);
}

render() {
const { src, isLimbo } = this.state;

if (isVignetteUrl(src)) {
// Limbo state happens when only the src and/or srcset is changed
// there is no standard on how to handle the state of the image when the src is changed across browsers
// lets just remove the entire node from html when in limbo
return (
<React.Fragment>
{!isLimbo && this.renderVignetteImage()}
{/* // support no-js and SSR */}
<noscript>
{this.renderPlainImage()}
</noscript>
</React.Fragment>
);
}

// if the image is not a Vignette one, just render it and don't care
return this.renderPlainImage();
}
}

export default Image;
43 changes: 43 additions & 0 deletions source/components/Image/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { mount } from 'enzyme';
import React from 'react';

import Image from './index';


// jest.mock('../ImagePreloader', () => mockComponent('ImagePreloader'));

function mountImage(additionalProps = {}) {
const wrapper = mount(
<Image
alt="test"
src="http://vignette.wikia-dev.us/22b12324-ab36-4266-87c9-452776157205"
{...additionalProps}
/>,
);
return wrapper;
}

test('Image test', () => {
const component = mountImage();
expect(component).toMatchSnapshot();
});

test('Image test for non-vignette', () => {
const component = mountImage({ src: 'https://foo.jpg' });
expect(component).toMatchSnapshot();
});

test('Updating an image src', () => {
const component = mountImage();

component.setState({ src: 'test' });
component.update();

expect(component).toMatchSnapshot();
});

test('Disabled lazyLoading', () => {
const component = mountImage({ disableLazy: true });

expect(component).toMatchSnapshot();
});
30 changes: 30 additions & 0 deletions source/components/ImagePreloader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
`ImagePreloader` component can be used to load (or preload) any image. Both `onChange` and children are functions that are called with `ImagePreloader`'s state with the following:

* `state` - the current state of the preloading - can be either `pending`, `success` or `error`
All the states are xported via `ImagePreloader.STATE` const.
* `error` - a JavaScript `Error` object (if the `state` is `error`) or null
* `src` - taken from `ImagePreloader`'s props
* `srcSet` - taken from `ImagePreloader`'s props

### Usage:

```js static
import ImagePreloader from '@wikia/react-common/components/ImagePreloader';

export default ImageWithLoadingMessage = () => (
<ImagePreloader src="http://path.to.image.jpg">
{({ state, src }) => {
if (state === ImagePreloader.STATE.PENDING) {
return <span>Loading image...</span>;
}

if (state === ImagePreloader.STATE.ERROR) {
return <span>Error loading image</span>;
}

return <img src={src} />;
}}
</ImagePreloader>
);

```
Loading