diff options
author | Eugene Sokolov <eug-vs@keemail.me> | 2020-08-14 16:58:41 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-14 16:58:41 +0300 |
commit | 9073af9512687e724a5237d55a85656abb3305e5 (patch) | |
tree | d4a96f03365fac6f36b3826e2c18c24ee3db0974 /src/components/Image | |
parent | a6d2d1d833116772178125b3e047e0811131c0e0 (diff) | |
parent | d309f375af53dbb3415e2f892dc85e495ea0cf4c (diff) | |
download | which-ui-9073af9512687e724a5237d55a85656abb3305e5.tar.gz |
Merge pull request #82 from which-ecosystem/better-ux
Material images (loading, transitions, error)
Diffstat (limited to 'src/components/Image')
-rw-r--r-- | src/components/Image/BackgroundImage.tsx | 35 | ||||
-rw-r--r-- | src/components/Image/Image.tsx | 100 |
2 files changed, 135 insertions, 0 deletions
diff --git a/src/components/Image/BackgroundImage.tsx b/src/components/Image/BackgroundImage.tsx new file mode 100644 index 0000000..e749d10 --- /dev/null +++ b/src/components/Image/BackgroundImage.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Image from './Image'; + +interface PropTypes { + src?: string; + alt?: string; +} + +const useStyles = makeStyles({ + root: { + position: 'absolute', + width: '100%', + height: '100%' + }, + image: { + objectFit: 'cover', + pointerEvents: 'none', + width: '100%', + height: '100%' + } +}); + +const BackgroundImage: React.FC<PropTypes> = ({ src, alt }) => { + const classes = useStyles(); + + return ( + <picture className={classes.root}> + <Image src={src} alt={alt} className={classes.image} /> + </picture> + ); +}; + +export default BackgroundImage; + diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx new file mode 100644 index 0000000..1898716 --- /dev/null +++ b/src/components/Image/Image.tsx @@ -0,0 +1,100 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import ErrorIcon from '@material-ui/icons/BrokenImage'; +import grey from '@material-ui/core/colors/grey'; + +interface PropTypes { + src?: string; + alt?: string; + className?: string; + animationDuration?: number; + disableLoading?: boolean; +} + +type Status = 'success' | 'loading' | 'error'; + +const useStyles = makeStyles(theme => ({ + container: { + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + success: { + opacity: '100%', + filterBrightness: '100%', + filterSaturate: '100%' + }, + loading: { + opacity: 0, + filterBrightness: 0, + filterSaturate: '20%', + position: 'absolute' + }, + error: { + display: 'none' + }, + errorIcon: { + color: grey[300], + width: theme.spacing(6), + height: theme.spacing(6) + } +})); + +const Image: React.FC<PropTypes> = React.memo(({ + src, + alt, + className, + animationDuration = 1000, + disableLoading = false +}) => { + const classes = useStyles(); + const [status, setStatus] = useState<Status>('loading'); + + const handleLoad = useCallback((): void => { + setStatus('success'); + }, [setStatus]); + + const handleError = useCallback((): void => { + setStatus('error'); + }, [setStatus]); + + const transition = useMemo(() => ` + filterBrightness ${animationDuration * 0.75}ms cubic-bezier(0.4, 0.0, 0.2, 1), + filterSaturate ${animationDuration}ms cubic-bezier(0.4, 0.0, 0.2, 1), + opacity ${animationDuration / 2}ms cubic-bezier(0.4, 0.0, 0.2, 1) + `, [animationDuration]); + + const image = ( + <img + src={src} + alt={alt} + className={`${className} ${classes[status]}`} + style={{ transition }} + onLoad={handleLoad} + onError={handleError} + /> + ); + + return ( + <> + {src && image} + { + status !== 'success' && ( + <div className={classes.container}> + { + (status === 'error' || !src) + ? <ErrorIcon className={classes.errorIcon} /> + : (!disableLoading && <CircularProgress color="primary" />) + } + </div> + ) + } + </> + ); +}); + +export default Image; + |