From 112e0cadc758c0557d59abcc7a08d93a4c48f302 Mon Sep 17 00:00:00 2001 From: Neto Chaves Date: Fri, 6 Dec 2019 22:00:43 -0300 Subject: [PATCH 1/3] fallback image when fails to load --- packages/material-ui/src/Avatar/Avatar.js | 69 +++++++++++++++++-- .../src/internal/svg-icons/Person.js | 10 +++ 2 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 packages/material-ui/src/internal/svg-icons/Person.js diff --git a/packages/material-ui/src/Avatar/Avatar.js b/packages/material-ui/src/Avatar/Avatar.js index 95347e73d6bd61..bdca45b909cacb 100644 --- a/packages/material-ui/src/Avatar/Avatar.js +++ b/packages/material-ui/src/Avatar/Avatar.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import withStyles from '../styles/withStyles'; +import Person from '../internal/svg-icons/Person'; export const styles = theme => ({ /* Styles applied to the root element. */ @@ -20,7 +21,7 @@ export const styles = theme => ({ overflow: 'hidden', userSelect: 'none', }, - /* Styles applied to the root element if there are children and not `src` or `srcSet`. */ + /* Styles applied to the root element if not `src` or `srcSet`. */ colorDefault: { color: theme.palette.background.default, backgroundColor: @@ -43,9 +44,55 @@ export const styles = theme => ({ textAlign: 'center', // Handle non-square image. The property isn't supported by IE 11. objectFit: 'cover', + // Hide alt text. + color: 'transparent', + // Same color as the Skeleton. + backgroundColor: theme.palette.action.hover, + // Hide the image broken icon, only works on Chrome. + textIndent: 10000, + }, + /* Styles applied to the fallback icon */ + fallback: { + width: '80%', + height: '80%', }, }); +const useLoaded = ({ src, srcSet }) => { + const [loaded, setLoaded] = React.useState(false); + + React.useEffect(() => { + if (!src && !srcSet) { + return undefined; + } + + setLoaded(false); + + let active = true; + const image = new Image(); + image.src = src; + image.srcSet = srcSet; + image.onload = () => { + if (!active) { + return; + } + setLoaded('loaded'); + }; + image.onerror = () => { + if (!active) { + return; + } + setLoaded('error'); + }; + + return () => { + active = false; + }; + }, [src, srcSet]); + + return loaded; +}; + const Avatar = React.forwardRef(function Avatar(props, ref) { const { alt, @@ -62,9 +109,13 @@ const Avatar = React.forwardRef(function Avatar(props, ref) { } = props; let children = null; - const img = src || srcSet; - if (img) { + // Use a hook instead of onError on the img element to support server-side rendering. + const loaded = useLoaded({ src, srcSet }); + const hasImg = src || srcSet; + const hasImgNotFailing = hasImg && loaded !== 'error'; + + if (hasImgNotFailing) { children = ( {alt} ); - } else { + } else if (childrenProp) { children = childrenProp; + } else if (hasImg && alt) { + children = alt[0]; + } else { + children = ; } return ( @@ -86,7 +141,7 @@ const Avatar = React.forwardRef(function Avatar(props, ref) { classes.system, classes[variant], { - [classes.colorDefault]: !img, + [classes.colorDefault]: !hasImgNotFailing, }, className, )} @@ -124,8 +179,8 @@ Avatar.propTypes = { */ component: PropTypes.elementType, /** - * Attributes applied to the `img` element if the component - * is used to display an image. + * Attributes applied to the `img` element if the component is used to display an image. + * It can be used to listen for the loading error event. */ imgProps: PropTypes.object, /** diff --git a/packages/material-ui/src/internal/svg-icons/Person.js b/packages/material-ui/src/internal/svg-icons/Person.js new file mode 100644 index 00000000000000..43d267a1af70ef --- /dev/null +++ b/packages/material-ui/src/internal/svg-icons/Person.js @@ -0,0 +1,10 @@ +import React from 'react'; +import createSvgIcon from './createSvgIcon'; + +/** + * @ignore - internal component. + */ +export default createSvgIcon( + , + 'Person', +); From b7a6f916ab38e92669d34f60b893a02949a377bd Mon Sep 17 00:00:00 2001 From: Neto Chaves Date: Fri, 6 Dec 2019 22:08:46 -0300 Subject: [PATCH 2/3] yarn docs:api --- docs/pages/api/avatar.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pages/api/avatar.md b/docs/pages/api/avatar.md index 6db035aa37a66e..603306698672cc 100644 --- a/docs/pages/api/avatar.md +++ b/docs/pages/api/avatar.md @@ -28,7 +28,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi | children | node | | Used to render icon or text elements inside the Avatar if `src` is not set. This can be an element, or just a string. | | classes | object | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. | | component | elementType | 'div' | The component used for the root node. Either a string to use a DOM element or a component. | -| imgProps | object | | Attributes applied to the `img` element if the component is used to display an image. | +| imgProps | object | | Attributes applied to the `img` element if the component is used to display an image. It can be used to listen for the loading error event. | | sizes | string | | The `sizes` attribute for the `img` element. | | src | string | | The `src` attribute for the `img` element. | | srcSet | string | | The `srcSet` attribute for the `img` element. Use this attribute for responsive image display. | @@ -46,11 +46,12 @@ Any other props supplied will be provided to the root element (native element). | Rule name | Global class | Description | |:-----|:-------------|:------------| | root | .MuiAvatar-root | Styles applied to the root element. -| colorDefault | .MuiAvatar-colorDefault | Styles applied to the root element if there are children and not `src` or `srcSet`. +| colorDefault | .MuiAvatar-colorDefault | Styles applied to the root element if not `src` or `srcSet`. | circle | .MuiAvatar-circle | Styles applied to the root element if `variant="circle"`. | rounded | .MuiAvatar-rounded | Styles applied to the root element if `variant="rounded"`. | square | .MuiAvatar-square | Styles applied to the root element if `variant="square"`. | img | .MuiAvatar-img | Styles applied to the img element if either `src` or `srcSet` is defined. +| fallback | .MuiAvatar-fallback | Styles applied to the fallback icon You can override the style of the component thanks to one of these customization points: From fa995f0890494c931c592f34820957ac82b3e837 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 7 Dec 2019 12:57:20 +0100 Subject: [PATCH 3/3] add demo --- .../components/avatars/FallbackAvatars.js | 31 +++++++++++++++++ .../components/avatars/FallbackAvatars.tsx | 33 +++++++++++++++++++ docs/src/pages/components/avatars/avatars.md | 10 ++++++ packages/material-ui/src/Avatar/Avatar.js | 10 +++--- test/utils/createDOM.js | 1 + 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 docs/src/pages/components/avatars/FallbackAvatars.js create mode 100644 docs/src/pages/components/avatars/FallbackAvatars.tsx diff --git a/docs/src/pages/components/avatars/FallbackAvatars.js b/docs/src/pages/components/avatars/FallbackAvatars.js new file mode 100644 index 00000000000000..808c5d3e812632 --- /dev/null +++ b/docs/src/pages/components/avatars/FallbackAvatars.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Avatar from '@material-ui/core/Avatar'; +import { deepOrange } from '@material-ui/core/colors'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + '& > *': { + margin: theme.spacing(1), + }, + }, + orange: { + color: theme.palette.getContrastText(deepOrange[500]), + backgroundColor: deepOrange[500], + }, +})); + +export default function FallbackAvatars() { + const classes = useStyles(); + + return ( +
+ + B + + + +
+ ); +} diff --git a/docs/src/pages/components/avatars/FallbackAvatars.tsx b/docs/src/pages/components/avatars/FallbackAvatars.tsx new file mode 100644 index 00000000000000..174e0ee9cb2e96 --- /dev/null +++ b/docs/src/pages/components/avatars/FallbackAvatars.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; +import Avatar from '@material-ui/core/Avatar'; +import { deepOrange } from '@material-ui/core/colors'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + display: 'flex', + '& > *': { + margin: theme.spacing(1), + }, + }, + orange: { + color: theme.palette.getContrastText(deepOrange[500]), + backgroundColor: deepOrange[500], + }, + }), +); + +export default function FallbackAvatars() { + const classes = useStyles(); + + return ( +
+ + B + + + +
+ ); +} diff --git a/docs/src/pages/components/avatars/avatars.md b/docs/src/pages/components/avatars/avatars.md index 6cd169f3f056b5..a3cfa8fc434772 100644 --- a/docs/src/pages/components/avatars/avatars.md +++ b/docs/src/pages/components/avatars/avatars.md @@ -30,3 +30,13 @@ Icon avatars are created by passing an icon as `children`. If you need square or rounded avatars, use the `variant` prop. {{"demo": "pages/components/avatars/VariantAvatars.js"}} + +## Fallbacks + +The component fallbacks if there is an error loading the avatar image, in this order, to: + +- the provided children +- the first letter of tha `alt` text +- a generic avatar icon + +{{"demo": "pages/components/avatars/FallbackAvatars.js"}} diff --git a/packages/material-ui/src/Avatar/Avatar.js b/packages/material-ui/src/Avatar/Avatar.js index bdca45b909cacb..9505a20f6ba73b 100644 --- a/packages/material-ui/src/Avatar/Avatar.js +++ b/packages/material-ui/src/Avatar/Avatar.js @@ -53,12 +53,12 @@ export const styles = theme => ({ }, /* Styles applied to the fallback icon */ fallback: { - width: '80%', - height: '80%', + width: '75%', + height: '75%', }, }); -const useLoaded = ({ src, srcSet }) => { +function useLoaded({ src, srcSet }) { const [loaded, setLoaded] = React.useState(false); React.useEffect(() => { @@ -91,7 +91,7 @@ const useLoaded = ({ src, srcSet }) => { }, [src, srcSet]); return loaded; -}; +} const Avatar = React.forwardRef(function Avatar(props, ref) { const { @@ -126,7 +126,7 @@ const Avatar = React.forwardRef(function Avatar(props, ref) { {...imgProps} /> ); - } else if (childrenProp) { + } else if (childrenProp != null) { children = childrenProp; } else if (hasImg && alt) { children = alt[0]; diff --git a/test/utils/createDOM.js b/test/utils/createDOM.js index 3b9a8cedf71aa7..4e0abf622a598d 100644 --- a/test/utils/createDOM.js +++ b/test/utils/createDOM.js @@ -6,6 +6,7 @@ const whitelist = [ // required for fake getComputedStyle 'CSSStyleDeclaration', 'Element', + 'Image', 'HTMLElement', 'HTMLInputElement', 'Performance',