Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Avatar] Fallback images when fails to load #18711

Merged
merged 3 commits into from
Dec 7, 2019
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
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