Skip to content

Commit

Permalink
[Avatar] Fallback images when fails to load (#18711)
Browse files Browse the repository at this point in the history
  • Loading branch information
netochaves authored and oliviertassinari committed Dec 7, 2019
1 parent e8703bf commit f8464a1
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 9 deletions.
5 changes: 3 additions & 2 deletions docs/pages/api/avatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | Used to render icon or text elements inside the Avatar if `src` is not set. This can be an element, or just a string. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'div'</span> | The component used for the root node. Either a string to use a DOM element or a component. |
| <span class="prop-name">imgProps</span> | <span class="prop-type">object</span> | | Attributes applied to the `img` element if the component is used to display an image. |
| <span class="prop-name">imgProps</span> | <span class="prop-type">object</span> | | 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. |
| <span class="prop-name">sizes</span> | <span class="prop-type">string</span> | | The `sizes` attribute for the `img` element. |
| <span class="prop-name">src</span> | <span class="prop-type">string</span> | | The `src` attribute for the `img` element. |
| <span class="prop-name">srcSet</span> | <span class="prop-type">string</span> | | The `srcSet` attribute for the `img` element. Use this attribute for responsive image display. |
Expand All @@ -46,11 +46,12 @@ Any other props supplied will be provided to the root element (native element).
| Rule name | Global class | Description |
|:-----|:-------------|:------------|
| <span class="prop-name">root</span> | <span class="prop-name">.MuiAvatar-root</span> | Styles applied to the root element.
| <span class="prop-name">colorDefault</span> | <span class="prop-name">.MuiAvatar-colorDefault</span> | Styles applied to the root element if there are children and not `src` or `srcSet`.
| <span class="prop-name">colorDefault</span> | <span class="prop-name">.MuiAvatar-colorDefault</span> | Styles applied to the root element if not `src` or `srcSet`.
| <span class="prop-name">circle</span> | <span class="prop-name">.MuiAvatar-circle</span> | Styles applied to the root element if `variant="circle"`.
| <span class="prop-name">rounded</span> | <span class="prop-name">.MuiAvatar-rounded</span> | Styles applied to the root element if `variant="rounded"`.
| <span class="prop-name">square</span> | <span class="prop-name">.MuiAvatar-square</span> | Styles applied to the root element if `variant="square"`.
| <span class="prop-name">img</span> | <span class="prop-name">.MuiAvatar-img</span> | Styles applied to the img element if either `src` or `srcSet` is defined.
| <span class="prop-name">fallback</span> | <span class="prop-name">.MuiAvatar-fallback</span> | Styles applied to the fallback icon

You can override the style of the component thanks to one of these customization points:

Expand Down
31 changes: 31 additions & 0 deletions docs/src/pages/components/avatars/FallbackAvatars.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes.root}>
<Avatar alt="Remy Sharp" src="/broken-image.jpg" className={classes.orange}>
B
</Avatar>
<Avatar alt="Remy Sharp" src="/broken-image.jpg" className={classes.orange} />
<Avatar src="/broken-image.jpg" />
</div>
);
}
33 changes: 33 additions & 0 deletions docs/src/pages/components/avatars/FallbackAvatars.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes.root}>
<Avatar alt="Remy Sharp" src="/broken-image.jpg" className={classes.orange}>
B
</Avatar>
<Avatar alt="Remy Sharp" src="/broken-image.jpg" className={classes.orange} />
<Avatar src="/broken-image.jpg" />
</div>
);
}
10 changes: 10 additions & 0 deletions docs/src/pages/components/avatars/avatars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
69 changes: 62 additions & 7 deletions packages/material-ui/src/Avatar/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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:
Expand All @@ -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: '75%',
height: '75%',
},
});

function 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,
Expand All @@ -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 = (
<img
alt={alt}
Expand All @@ -75,8 +126,12 @@ const Avatar = React.forwardRef(function Avatar(props, ref) {
{...imgProps}
/>
);
} else {
} else if (childrenProp != null) {
children = childrenProp;
} else if (hasImg && alt) {
children = alt[0];
} else {
children = <Person className={classes.fallback} />;
}

return (
Expand All @@ -86,7 +141,7 @@ const Avatar = React.forwardRef(function Avatar(props, ref) {
classes.system,
classes[variant],
{
[classes.colorDefault]: !img,
[classes.colorDefault]: !hasImgNotFailing,
},
className,
)}
Expand Down Expand Up @@ -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,
/**
Expand Down
10 changes: 10 additions & 0 deletions packages/material-ui/src/internal/svg-icons/Person.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import createSvgIcon from './createSvgIcon';

/**
* @ignore - internal component.
*/
export default createSvgIcon(
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />,
'Person',
);
1 change: 1 addition & 0 deletions test/utils/createDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const whitelist = [
// required for fake getComputedStyle
'CSSStyleDeclaration',
'Element',
'Image',
'HTMLElement',
'HTMLInputElement',
'Performance',
Expand Down

0 comments on commit f8464a1

Please sign in to comment.