Skip to content
Draft
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
30 changes: 25 additions & 5 deletions src/Avatar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,36 @@ import React from 'react';
import PropTypes from 'prop-types';

import { AvatarIcon } from './Icons';
import { getAvatarColor, getInitial } from './avatarUtils';

const Avatar = ({
size,
src,
alt,
className,
username,
}) => {
const avatar = src ? (
<img className="d-block w-100 h-100" src={src} alt={alt} />
) : (
<AvatarIcon style={{ width: size, height: size }} role="img" aria-hidden focusable="false" />
);
let avatar;
if (src) {
avatar = <img className="d-block w-100 h-100" src={src} alt={alt} />;
} else if (username) {
avatar = (
<span
className="d-flex w-100 h-100 align-items-center justify-content-center font-weight-bold"
style={{
backgroundColor: getAvatarColor(username),
color: '#FFFFFF',
fontSize: `calc(${size} * 0.55)`,
}}
role="img"
aria-hidden
>
{getInitial(username)}
</span>
);
} else {
avatar = <AvatarIcon style={{ width: size, height: size }} role="img" aria-hidden focusable="false" />;
}

return (
<span
Expand All @@ -30,13 +48,15 @@ Avatar.propTypes = {
size: PropTypes.string,
alt: PropTypes.string,
className: PropTypes.string,
username: PropTypes.string,
};

Avatar.defaultProps = {
src: null,
size: '2rem',
alt: null,
className: null,
username: null,
};

export default Avatar;
31 changes: 31 additions & 0 deletions src/avatarUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Paragon light-theme tokens (primary/brand/success/info/danger families),
// chosen for WCAG-safe contrast with white text.
export const AVATAR_COLORS = [
'#0A3055', // primary
'#9D0054', // brand
'#178253', // success (green)
'#006DAA', // info (teal)
'#C32D3A', // danger (red)
'#476480', // primary-400
'#B6407F', // brand-400
'#15754B', // success-600
'#006299', // info-600
'#B02934', // danger-600
];

/**
* Returns a deterministic background color for the given username.
* Same username always maps to the same palette color.
*/
export const getAvatarColor = (username) => {
let hash = 0;
for (let i = 0; i < username.length; i += 1) {
hash = ((hash << 5) - hash + username.charCodeAt(i)) | 0; // eslint-disable-line no-bitwise
}
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
};

/**
* Returns the uppercased first character of the username.
*/
export const getInitial = (username) => username.charAt(0).toUpperCase();
2 changes: 1 addition & 1 deletion src/desktop-header/DesktopUserMenuToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Avatar from '../Avatar';

const DesktopUserMenuToggle = ({ avatar, label }) => (
<>
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" username={label} />
{label} <CaretIcon role="img" aria-hidden focusable="false" />
</>
);
Expand Down
5 changes: 4 additions & 1 deletion src/mobile-header/MobileUserMenuToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import Avatar from '../Avatar';

const MobileUserMenuToggle = ({ avatar, username }) => <Avatar size="1.5rem" src={avatar} alt={username} />;
const MobileUserMenuToggle = ({ avatar, username, label }) => (
<Avatar size="1.5rem" src={avatar} alt={username || label} username={username || label} />
);

export const MobileUserMenuTogglePropTypes = {
avatar: PropTypes.string,
username: PropTypes.string,
label: PropTypes.string,
};

MobileUserMenuToggle.propTypes = MobileUserMenuTogglePropTypes;
Expand Down
18 changes: 9 additions & 9 deletions src/studio-header/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Avatar,
} from '@openedx/paragon';
import Avatar from '../Avatar';
import NavDropdownMenu from './NavDropdownMenu';
import getUserMenuItems from './utils';

Expand All @@ -24,12 +22,14 @@ const UserMenu = ({
data-testid="avatar-image"
/>
) : (
<Avatar
size="sm"
className="mr-2"
alt={username}
data-testid="avatar-icon"
/>
<span data-testid="avatar-icon">
<Avatar
size="1.5rem"
className="mr-2"
alt={username}
username={username}
/>
</span>
);
const title = isMobile ? avatar : <>{avatar}{username}</>;

Expand Down
Loading