├── .editorconfig
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── demo
└── src
│ └── index.js
├── nwb.config.js
├── package-lock.json
├── package.json
├── src
├── Image.js
└── index.js
└── tests
├── .eslintrc
└── index.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = tab
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 | # editorconfig-tools is unable to ignore longs strings or urls
12 | max_line_length = off
13 |
14 | [*.md]
15 | max_line_length = 0
16 | trim_trailing_whitespace = false
17 |
18 | [COMMIT_EDITMSG]
19 | max_line_length = 0
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | npm-debug.log*
8 | .DS_Store
9 | .vscode
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/prettierrc",
3 | "printWidth": 80,
4 | "arrowParens": "always",
5 | "bracketSpacing": true,
6 | "jsxBracketSameLine": false,
7 | "jsxSingleQuote": false,
8 | "singleQuote": true,
9 | "semi": true,
10 | "trailingComma": "es5",
11 | "useEditorConfig": true
12 | }
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 | node_js:
5 | - 10
6 |
7 | before_install:
8 | - npm install codecov.io coveralls
9 |
10 | after_success:
11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
13 |
14 | branches:
15 | only:
16 | - master
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | [Node.js](http://nodejs.org/) >= 10 must be installed.
4 |
5 | ## Installation
6 |
7 | - Running `npm install` in the component's root directory will install everything you need for development.
8 |
9 | ## Demo Development Server
10 |
11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 |
13 | ## Running Tests
14 |
15 | - `npm test` will run the tests once.
16 |
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 |
19 | - `npm run test:watch` will run the tests on every change.
20 |
21 | ## Building
22 |
23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
24 |
25 | - `npm run clean` will delete built resources.
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2022 [benmneb](https://github.com/benmneb/)
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
🌅
mui-image
3 |
4 |
5 | The only Material UI image component to satisfy the Material Design guidelines for loading images.
6 |
7 |
8 |
9 |
10 |
11 |
12 | Demo Playground ↗️
13 |
14 |
15 | ### If you're already using [Material UI v5](https://material-ui.com), why not display your images according to the Material guidelines too?
16 |
17 | > Illustrations and photographs may load and transition in three phases at staggered durations, rather than relying on opacity changes alone.
18 | >
19 | > Visualize the image fading in, like a print during the photo development process.
20 | >
21 | > \- [Material guidelines](https://material.io/archive/guidelines/patterns/loading-images.html#loading-images-usage)
22 |
23 | ### 1. Install
24 |
25 | ```
26 | npm i mui-image
27 | ```
28 |
29 | or
30 |
31 | ```
32 | yarn add mui-image
33 | ```
34 |
35 | Using TypeScript? Also add [`@types/mui-image`](https://www.npmjs.com/package/@types/mui-image) 🥳
36 |
37 | ### 2. Use
38 |
39 | ```
40 | import Image from 'mui-image'
41 |
42 | // or
43 |
44 | import { Image } from 'mui-image'
45 |
46 | // then
47 |
48 |
49 | ```
50 |
51 | ### 3. Profit 💰
52 |
53 | _Note: Profits not guaranteed and Material UI v5 is a peer dependency. If you need to support legacy versions of Material UI, use [`material-ui-image`](https://github.com/TeamWertarbyte/material-ui-image) instead. See the [comparison chart](#comparison-with-similar-components) below for more._
54 |
55 | ## Usage Examples
56 |
57 | You can use `mui-image` like a regular image.
58 |
59 | ```
60 |
61 | ```
62 |
63 | Except... it will fade and animate in as the Material guidelines recommend. 🤯
64 |
65 | Add a `height` and/or `width` to reserve space on the page for the image and avoid uncomforable content shifts as your picture loads. They both default to 100% of the parent you place them in and accept any valid CSS property. Numbers are converted to pixels.
66 |
67 | ```
68 |
69 |
70 | ```
71 |
72 | Apply the `showLoading` prop to add a progress indicator to let your fans know something amazing is coming. You can use the default Material UI indicator or bring your own. 😎
73 |
74 | ```
75 |
76 | } />
77 | ```
78 |
79 | If you want the image to fail silently you can disable the `errorIcon`, or you can add your own to suit your brand.
80 |
81 | ```
82 |
83 | } />
84 | ```
85 |
86 | If you want to _disobey Google_ 😵 then you can customise the animation and speed via the `duration` and `easing` props to any valid CSS property. Duration is always milliseconds.
87 |
88 | ```
89 |
90 |
91 | ```
92 |
93 | To add that extra bit of spice 🌶 you can do exactly what Google suggests and apply a small position `shift` to images as they appear. The direction, distance, and duration (in milliseconds) are up to you.
94 |
95 | ```
96 |
97 |
98 |
99 | ```
100 |
101 | And of course, you can style `mui-image` like you would a regular image... but with the addition of the Material UI v5 `sx` prop and [all the benefits](https://mui.com/system/the-sx-prop/) it brings. 😏
102 |
103 | ```
104 |
105 |
106 |
107 | ```
108 |
109 | If you want to get fancy 💃 you can also add inline styles and additional `className`'s to the root wrapper `div` and loading/error icon wrapper `div`, or just target their default `className`'s. This allows for complete customisation of every aspect of the component.
110 |
111 | Like and subscribe below for more. ⏬
112 |
113 | ## Props
114 |
115 | | Name | Type | Default | Description |
116 | | -------------------- | ---------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
117 | | alt | string | "" | image `alt` tag value |
118 | | bgColor | string | "inherit" | the color the image transitions in from |
119 | | className | string | "mui-image-img" | CSS `class` for the image |
120 | | distance | string / number | 100 | any valid [CSS `length` value](https://developer.mozilla.org/en-US/docs/Web/CSS/length#units) (for the shift) |
121 | | duration | number | 3000 | sets the CSS [`transition-duration`](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-duration) in milliseconds |
122 | | easing | string | cubic-bezier(0.7, 0, 0.6, 1) | sets the CSS [`transition-timing-function`](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) |
123 | | errorIcon | boolean / node | true | display default error icon, or your own |
124 | | fit | string | "contain" | any valid [CSS `object-fit` value](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit#syntax) |
125 | | height | number / string | "100%" | any valid [CSS `height` value](https://developer.mozilla.org/en-US/docs/Web/CSS/height) |
126 | | iconWrapperClassName | string | "mui-image-iconWrapper" | CSS `class` for the icon wrapper `div` |
127 | | iconWrapperStyle | object | | inline styles for the icon wrapper `div` |
128 | | position | string | "relative" | any valid [CSS `position` value](https://developer.mozilla.org/en-US/docs/Web/CSS/position) |
129 | | shift | boolean / string | false | either "left", "right", "top", "bottom", `null`, or `false` |
130 | | shiftDuration | number | duration \* 0.3 | duration of shift in milliseconds |
131 | | showLoading | boolean / node | false | display default loading spinner, or your own |
132 | | **_src_** \* | string | | image `src` tag... _required_ |
133 | | style | object | | inline styles for the image |
134 | | width | number / string | "100%" | any valid [CSS `width` value](https://developer.mozilla.org/en-US/docs/Web/CSS/width) |
135 | | wrapperClassName | string | "mui-image-wrapper" | CSS `class` for the root wrapper `div` |
136 | | wrapperStyle | object | | inline styles for the root wrapper `div` |
137 |
138 | \* required prop
139 |
140 | Any other props (eg. `sx`, `onLoad`) are passed directly to the native `img` element.
141 |
142 | ## Material guidelines for loading images
143 |
144 | > #### ✅ Fade-in
145 | >
146 | > Visualize the image fading in, like a print during the photo development process.
147 |
148 | > #### ✅ Opacity, exposure, and saturation recommendations
149 | >
150 | > Images should begin loading with low contrast levels and desaturated color. Once image opacity reaches 100%, display the image with full-color saturation.
151 |
152 | > #### ✅ Duration
153 | >
154 | > A longer duration is recommended for loading images, and a shorter duration is recommended for transitions.
155 |
156 | > #### ✅ Animation
157 | >
158 | > Add a small position shift to loading images.
159 |
160 | [(Source)](https://material.io/archive/guidelines/patterns/loading-images.html#loading-images-behavior)
161 |
162 | ## Comparison with similar components
163 |
164 | | Feature | `mui-image` | `material-ui-image` |
165 | | ----------------------------- | :------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: |
166 | | Size (minzipped) |  |  |
167 | | Supports MUI v5 | ✅ | ❌ |
168 | | Fade-in | ✅ | ✅ |
169 | | Progressive level adjustments | ✅ | ❌ |
170 | | Suggested duration | ✅ | ✅ |
171 | | Optional shift animation | ✅ | ❌ |
172 | | Supports legacy MUI versions | ❌ | ✅ |
173 |
174 | ## License
175 |
176 | © [benmneb](https://github.com/benmneb)
177 |
178 | [ISC License](https://choosealicense.com/licenses/isc/)
179 |
180 | Permission to use, copy, modify, and/or distribute this software for any
181 | purpose with or without fee is hereby granted, provided that the above
182 | copyright notice and this permission notice appear in all copies.
183 |
184 |
191 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { render } from 'react-dom';
4 |
5 | import TypeIt from 'typeit-react';
6 |
7 | import Image from '../../src';
8 |
9 | import {
10 | styled,
11 | AppBar,
12 | Box,
13 | Button,
14 | CssBaseline,
15 | Drawer,
16 | FormControlLabel,
17 | IconButton,
18 | MenuItem,
19 | Select,
20 | Stack,
21 | Switch,
22 | TextField,
23 | Tooltip,
24 | Typography,
25 | Toolbar,
26 | useMediaQuery,
27 | } from '@mui/material';
28 | import { createTheme, ThemeProvider } from '@mui/material/styles';
29 | import { createSvgIcon } from '@mui/material/utils';
30 |
31 | import '@fontsource/fira-code';
32 |
33 | const theme = createTheme({
34 | typography: {
35 | fontFamily: ['Fira Code', 'monospace'].join(','),
36 | },
37 | palette: {
38 | primary: { main: '#2979ff' },
39 | },
40 | });
41 |
42 | const YarnIcon = createSvgIcon(
43 | ,
44 | 'YarnIcon'
45 | );
46 |
47 | const NpmIcon = createSvgIcon(
48 | ,
49 | 'NpmIcon'
50 | );
51 |
52 | const GitHubIcon = createSvgIcon(
53 | ,
54 | 'GitHubIcon'
55 | );
56 |
57 | const CodeIcon = createSvgIcon(
58 | ,
59 | 'CodeIcon'
60 | );
61 |
62 | const CodeOffIcon = createSvgIcon(
63 | ,
64 | 'CodeOffIcon'
65 | );
66 |
67 | const Line = styled(Box)({
68 | display: 'flex',
69 | alignItems: 'center',
70 | '& .MuiTextField-root': {
71 | margin: '0 8px',
72 | },
73 | });
74 |
75 | const ImageOutput = styled('article')({
76 | display: 'flex',
77 | justifyContent: 'center',
78 | alignItems: 'center',
79 | height: '100%',
80 | overflow: 'hidden',
81 | });
82 |
83 | const DRAWER_WIDTH = 325;
84 | const DEFAULT_IMAGE = 674;
85 |
86 | const SHOW_LOADING = false;
87 | const ERROR_ICON = true;
88 | const HEIGHT = '100%';
89 | const WIDTH = '100%';
90 | const SHIFT = false;
91 | const DISTANCE = '100px';
92 | const SHIFT_DURATION = null;
93 | const DURATION = 3000;
94 | const EASING = 'cubic-bezier(0.7, 0, 0.6, 1)';
95 | const FIT = 'cover';
96 | const BG_COLOR = 'inherit';
97 |
98 | export default function Demo() {
99 | const [currentPhoto, setCurrentPhoto] = React.useState(DEFAULT_IMAGE);
100 | const [showPhoto, setShowPhoto] = React.useState(true);
101 |
102 | const [showLoading, setShowLoading] = React.useState(SHOW_LOADING);
103 | const [errorIcon, setErrorIcon] = React.useState(ERROR_ICON);
104 | const [height, setHeight] = React.useState(HEIGHT);
105 | const [width, setWidth] = React.useState(WIDTH);
106 | const [shift, setShift] = React.useState(SHIFT);
107 | const [distance, setDistance] = React.useState(DISTANCE);
108 | const [shiftDuration, setShiftDuration] = React.useState(SHIFT_DURATION);
109 | const [duration, setDuration] = React.useState(DURATION);
110 | const [easing, setEasing] = React.useState(EASING);
111 | const [fit, setFit] = React.useState(FIT);
112 | const [bgColor, setBgColor] = React.useState(BG_COLOR);
113 |
114 | function getNewPhoto() {
115 | if (mobileOpen) setMobileOpen(false);
116 | const newPhoto = Math.floor(Math.random() * 1051);
117 | setShowPhoto(false);
118 | setCurrentPhoto(newPhoto);
119 | setTimeout(() => {
120 | setShowPhoto(true);
121 | }, 100);
122 | }
123 |
124 | function refreshPhoto() {
125 | if (mobileOpen) setMobileOpen(false);
126 | setShowPhoto(false);
127 | setTimeout(() => {
128 | setShowPhoto(true);
129 | }, 100);
130 | }
131 |
132 | function resetDefaults() {
133 | setShowLoading(SHOW_LOADING);
134 | setErrorIcon(ERROR_ICON);
135 | setHeight(HEIGHT);
136 | setWidth(WIDTH);
137 | setShift(SHIFT);
138 | setDistance(DISTANCE);
139 | setShiftDuration(SHIFT_DURATION);
140 | setDuration(DURATION);
141 | setEasing(EASING);
142 | setFit(FIT);
143 | setBgColor(BG_COLOR);
144 | }
145 |
146 | const [mobileOpen, setMobileOpen] = React.useState(false);
147 |
148 | const mobile = useMediaQuery('@media (max-width: 900px)');
149 |
150 | function handleDrawerToggle() {
151 | setMobileOpen(!mobileOpen);
152 | }
153 |
154 | return (
155 |
156 |
157 | theme.zIndex.drawer + 1 }}
161 | >
162 |
163 |
170 | {mobileOpen ? : }
171 |
172 |
178 | {
180 | instance
181 | .pause(3500)
182 | .type('npm install mui-image')
183 | .pause(1500)
184 | .delete()
185 | .type("import Image from 'mui-image'");
186 |
187 | return instance;
188 | }}
189 | options={{ speed: 120, cursor: false }}
190 | />
191 |
192 |
198 | mui-image
199 |
200 |
201 |
203 | window.open('https://yarnpkg.com/package/mui-image')
204 | }
205 | color="inherit"
206 | >
207 |
212 |
213 | window.open('https://npmjs.com/package/mui-image')}
215 | color="inherit"
216 | >
217 |
218 |
219 |
221 | window.open('https://github.com/benmneb/mui-image')
222 | }
223 | color="inherit"
224 | >
225 |
226 |
227 |
228 |
229 |
230 |
248 |
249 |
255 |
256 | {'
258 |
259 |
260 | src="https://picsum.photos/id/{currentPhoto}/2000"
261 |
262 |
263 |
264 | height="
265 | setHeight(e.target.value)}
269 | />
270 | "
271 |
272 |
273 |
274 |
275 | width="
276 | setWidth(e.target.value)}
280 | />
281 | "
282 |
283 |
284 |
288 |
289 | fit=
290 |
302 |
303 |
304 |
308 |
309 | duration={'{'}
310 | setDuration(e.target.value)}
314 | />
315 | {'}'}
316 |
317 |
318 |
322 |
323 | easing=
324 |
339 |
340 |
341 |
345 |
346 | showLoading=
347 | setShowLoading(e.target.checked)}
353 | />
354 | }
355 | label={`{ ${showLoading} }`}
356 | labelPlacement="start"
357 | />
358 |
359 |
360 |
364 |
365 | errorIcon=
366 | setErrorIcon(e.target.checked)}
372 | />
373 | }
374 | label={`{ ${errorIcon} }`}
375 | labelPlacement="start"
376 | />
377 |
378 |
379 |
383 |
384 | shift=
385 |
397 |
398 |
399 |
403 |
404 | distance="
405 | setDistance(e.target.value)}
409 | />
410 | "
411 |
412 |
413 |
417 |
418 | shiftDuration={'{'}
419 | setShiftDuration(e.target.value)}
423 | />
424 | {'}'}
425 |
426 |
427 |
431 |
432 | bgColor="
433 | setBgColor(e.target.value)}
437 | />
438 | "
439 |
440 |
441 |
442 |
443 | {'/>'}
444 |
445 |
453 |
460 |
461 |
462 |
463 |
464 |
465 |
470 | {showPhoto && (
471 |
485 | )}
486 |
487 |
488 |
489 | );
490 | }
491 |
492 | render(
493 |
494 |
495 |
496 | ,
497 | document.querySelector('#demo')
498 | );
499 |
--------------------------------------------------------------------------------
/nwb.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'react-component',
3 | npm: {
4 | esModules: true,
5 | umd: {
6 | global: 'MuiImage',
7 | externals: {
8 | react: 'React',
9 | },
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mui-image",
3 | "version": "1.0.7",
4 | "description": "Display images as per the Material guidelines. For React apps using Material-UI v5.",
5 | "main": "lib/index.js",
6 | "module": "es/index.js",
7 | "files": [
8 | "css",
9 | "es",
10 | "lib",
11 | "umd"
12 | ],
13 | "scripts": {
14 | "build": "nwb build-react-component",
15 | "clean": "nwb clean-module && nwb clean-demo",
16 | "prepublishOnly": "npm run build",
17 | "start": "nwb serve-react-demo",
18 | "test": "nwb test-react",
19 | "test:coverage": "nwb test-react --coverage",
20 | "test:watch": "nwb test-react --server",
21 | "size": "size-limit"
22 | },
23 | "peerDependencies": {
24 | "@emotion/react": "^11.4.1",
25 | "@emotion/styled": "^11.3.0",
26 | "@mui/material": "^5.0.1",
27 | "prop-types": "^15.7.2",
28 | "react": "^17.0.2 || ^18.0.0"
29 | },
30 | "devDependencies": {
31 | "@fontsource/fira-code": "^4.5.1",
32 | "@size-limit/preset-small-lib": "^7.0.8",
33 | "nwb": "0.25.2",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "size-limit": "^7.0.8",
37 | "typeit-react": "^2.0.3"
38 | },
39 | "size-limit": [
40 | {
41 | "path": "es/index.js"
42 | }
43 | ],
44 | "author": "benmneb",
45 | "homepage": "https://mui-image.surge.sh/",
46 | "license": "ISC",
47 | "repository": "https://github.com/benmneb/mui-image",
48 | "keywords": [
49 | "material-ui",
50 | "material-ui image",
51 | "material-ui-image",
52 | "material design",
53 | "react",
54 | "loading image"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/src/Image.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import PropTypes from 'prop-types';
4 |
5 | import { styled } from '@mui/material/styles';
6 | import { createSvgIcon } from '@mui/material/utils';
7 | import CircularProgress from '@mui/material/CircularProgress';
8 |
9 | const BrokenImageIcon = createSvgIcon(
10 | ,
11 | 'BrokenImageIcon'
12 | );
13 |
14 | const Img = styled('img')({
15 | '@keyframes materialize': {
16 | '0%': {
17 | filter: 'saturate(20%) contrast(50%) brightness(120%)',
18 | },
19 | '75%': {
20 | filter: 'saturate(60%) contrast(100%) brightness(100%)',
21 | },
22 | '100%': {
23 | filter: 'saturate(100%) contrast(100%) brightness(100%)',
24 | },
25 | },
26 | });
27 |
28 | export default function Image({
29 | src,
30 | style,
31 | wrapperStyle,
32 | iconWrapperStyle,
33 | onLoad: onLoadProp,
34 | onError: onErrorProp,
35 | alt = '',
36 | height = '100%',
37 | width = '100%',
38 | position = 'relative',
39 | fit = 'cover',
40 | showLoading = false,
41 | errorIcon = true,
42 | shift = false,
43 | distance = 100,
44 | shiftDuration = null,
45 | bgColor = 'inherit',
46 | duration = 3000,
47 | easing = 'cubic-bezier(0.7, 0, 0.6, 1)', // "heavy move" from https://sprawledoctopus.com/easing/
48 | className = '',
49 | wrapperClassName = '',
50 | iconWrapperClassName = '',
51 | ...rest
52 | }) {
53 | const [loaded, setLoaded] = React.useState(false);
54 | const [error, setError] = React.useState(false);
55 |
56 | function handleLoad() {
57 | setLoaded(true);
58 | setError(false);
59 | if (Boolean(onLoadProp)) onLoadProp();
60 | }
61 |
62 | function handleError() {
63 | setError(true);
64 | setLoaded(false);
65 | if (Boolean(onErrorProp)) onErrorProp();
66 | }
67 |
68 | const shiftStyles = {
69 | [shift]: loaded ? 0 : distance,
70 | };
71 |
72 | const styles = {
73 | root: {
74 | width,
75 | height,
76 | display: 'flex',
77 | justifyContent: 'center',
78 | alignItems: 'center',
79 | backgroundColor: bgColor,
80 | ...wrapperStyle,
81 | },
82 | image: {
83 | position,
84 | width: '100%',
85 | height: '100%',
86 | objectFit: fit,
87 | transitionProperty: `${Boolean(shift) ? `${shift}, ` : ''}opacity`,
88 | transitionDuration: `${
89 | Boolean(shift) ? `${shiftDuration || duration * 0.3}ms, ` : ''
90 | }${duration / 2}ms`,
91 | transitionTimingFunction: easing,
92 | opacity: loaded ? 1 : 0,
93 | animation: loaded ? `materialize ${duration}ms 1 ${easing}` : '',
94 | ...(Boolean(shift) && shiftStyles),
95 | ...style,
96 | },
97 | icons: {
98 | width: '100%',
99 | marginLeft: '-100%',
100 | display: 'flex',
101 | justifyContent: 'center',
102 | alignItems: 'center',
103 | opacity: loaded ? 0 : 1,
104 | ...iconWrapperStyle,
105 | },
106 | };
107 |
108 | const showErrorIcon = (typeof errorIcon !== 'boolean' && errorIcon) || (
109 | // MUI grey[400]
110 | );
111 |
112 | const loadingIndicator = (typeof showLoading !== 'boolean' &&
113 | showLoading) || ;
114 |
115 | return (
116 |
120 |

129 | {(Boolean(showLoading) || Boolean(errorIcon)) && (
130 |
134 | {Boolean(errorIcon) && error && showErrorIcon}
135 | {Boolean(showLoading) && !error && !loaded && loadingIndicator}
136 |
137 | )}
138 |
139 | );
140 | }
141 |
142 | Image.propTypes = {
143 | src: PropTypes.string.isRequired,
144 | alt: PropTypes.string,
145 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
146 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
147 | style: PropTypes.object,
148 | className: PropTypes.string,
149 | showLoading: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
150 | errorIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
151 | shift: PropTypes.oneOf([false, null, 'top', 'bottom', 'left', 'right']),
152 | distance: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
153 | shiftDuration: PropTypes.number,
154 | bgColor: PropTypes.string,
155 | wrapperStyle: PropTypes.object,
156 | iconWrapperStyle: PropTypes.object,
157 | wrapperClassName: PropTypes.string,
158 | iconWrapperClassName: PropTypes.string,
159 | duration: PropTypes.number,
160 | easing: PropTypes.string,
161 | onLoad: PropTypes.func,
162 | onError: PropTypes.func,
163 | position: PropTypes.oneOf([
164 | 'static',
165 | 'relative',
166 | 'absolute',
167 | 'fixed',
168 | 'sticky',
169 | 'inherit',
170 | 'initial',
171 | 'revert',
172 | 'unset',
173 | ]),
174 | fit: PropTypes.oneOf([
175 | 'contain',
176 | 'cover',
177 | 'fill',
178 | 'none',
179 | 'scale-down',
180 | 'inherit',
181 | 'initial',
182 | 'revert',
183 | 'unset',
184 | ]),
185 | };
186 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Image from './Image';
2 |
3 | export default Image;
4 |
5 | export { default as Image } from './Image';
6 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import React from 'react';
3 | import { render, unmountComponentAtNode } from 'react-dom';
4 |
5 | import Component from 'src/';
6 |
7 | describe('Component', () => {
8 | let node;
9 |
10 | beforeEach(() => {
11 | node = document.createElement('div');
12 | });
13 |
14 | afterEach(() => {
15 | unmountComponentAtNode(node);
16 | });
17 |
18 | it('displays a welcome message', () => {
19 | render(, node, () => {
20 | expect(node.innerHTML).toContain('Welcome to React components');
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------