Skip to content

Commit f773aa3

Browse files
feat(annotation): Add icon indicator for AnnotationActivity (#3176)
* feat(annotation): Add icon indicator for AnnotationActivity * feat(annotation): Refactor badge icons to be part of Avatar * feat(annotation): Align badge sizes with avatar size As design system recommends, verified with Designers
1 parent ba66a34 commit f773aa3

File tree

19 files changed

+288
-56
lines changed

19 files changed

+288
-56
lines changed

src/components/avatar/Avatar.js.flow

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ type Props = {
1818
* Required if "name" is not specified.
1919
*/
2020
avatarUrl?: ?string,
21+
/**
22+
* Icon React Element that will be shown as a badge in bottom right corner of Avatar.
23+
*
24+
* Will not be used if `shouldShowExternal` and `isExternal` is true, then GlobalBadge will be shown.
25+
*/
26+
badgeIcon?: React.Element<any>,
2127
/** classname to add to the container element. */
2228
className?: string,
2329
/** Users id */
@@ -36,7 +42,16 @@ type Props = {
3642
size?: $Keys<typeof SIZES>,
3743
};
3844

39-
function Avatar({ avatarUrl, className, name, id, isExternal, shouldShowExternal = false, size = '' }: Props) {
45+
function Avatar({
46+
avatarUrl,
47+
badgeIcon,
48+
className,
49+
name,
50+
id,
51+
isExternal,
52+
shouldShowExternal = false,
53+
size = '',
54+
}: Props) {
4055
const [hasImageErrored, setHasImageErrored] = React.useState<boolean>(false);
4156
const [prevAvatarUrl, setPrevAvatarUrl] = React.useState<$PropertyType<Props, 'avatarUrl'>>(null);
4257

@@ -68,13 +83,15 @@ function Avatar({ avatarUrl, className, name, id, isExternal, shouldShowExternal
6883
avatar = <UnknownUserAvatar className="avatar-icon" />;
6984
}
7085

86+
let badge = null;
87+
if (shouldShowExternal && isExternal) {
88+
badge = <GlobeBadge16 className="bdl-Avatar-externalBadge" />;
89+
} else if (badgeIcon) {
90+
badge = <div className="bdl-Avatar-badge bdl-Avatar-iconBadge">{badgeIcon}</div>;
91+
}
92+
7193
return (
72-
<Badgeable
73-
className={classes}
74-
bottomRight={
75-
shouldShowExternal && isExternal ? <GlobeBadge16 className="bdl-Avatar-externalBadge" /> : undefined
76-
}
77-
>
94+
<Badgeable className={classes} bottomRight={badge}>
7895
<span role="presentation">{avatar}</span>
7996
</Badgeable>
8097
);

src/components/avatar/Avatar.scss

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@
5555
}
5656
}
5757

58+
&.avatar--iconBadge {
59+
.bdl-Avatar-iconBadge {
60+
border-width: 2px;
61+
62+
& > svg {
63+
width: 12px;
64+
height: 12px;
65+
}
66+
}
67+
68+
@include Badgeable-bottomRightBadge {
69+
bottom: -4px;
70+
left: calc(100% - 10px);
71+
}
72+
}
73+
5874
@for $i from 1 through length($avatar-colors) {
5975
.avatar-initials[data-bg-idx='#{$i - 1}'] {
6076
background-color: nth($avatar-colors, $i);
@@ -81,6 +97,20 @@
8197
left: calc(100% - 10px);
8298
}
8399
}
100+
101+
&.avatar--iconBadge {
102+
line-height: 0; // .badgeable-container has `line-height: 1 which evaluates to 13px which is too much for small badge
103+
104+
@include Badgeable-bottomRightBadge {
105+
bottom: -4px;
106+
left: calc(100% - 10px);
107+
}
108+
109+
.bdl-Avatar-iconBadge > svg {
110+
width: 8px;
111+
height: 8px;
112+
}
113+
}
84114
}
85115

86116
&.avatar--large {
@@ -90,9 +120,21 @@
90120
.avatar-initials {
91121
font-size: 14px;
92122
}
123+
124+
&.avatar--iconBadge {
125+
@include Badgeable-bottomRightBadge {
126+
left: calc(100% - 14px);
127+
}
128+
129+
.bdl-Avatar-iconBadge > svg {
130+
width: 16px;
131+
height: 16px;
132+
}
133+
}
93134
}
94135
}
95136

137+
.bdl-Avatar-badge,
96138
.bdl-Avatar-externalBadge {
97139
background-color: $white;
98140
border-color: $white;

src/components/avatar/Avatar.stories.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import * as React from 'react';
2+
import IconVerified from '../../icons/general/IconVerified';
3+
import IconTaskGeneral from '../../icons/two-toned/IconTaskGeneral';
4+
import IconAnnotation from '../../icons/two-toned/IconAnnotation';
5+
26
import Avatar from './Avatar';
37
import notes from './Avatar.stories.md';
48

@@ -34,6 +38,47 @@ export const markedAsExternal = () => (
3438
</div>
3539
);
3640

41+
export const withBadges = () => (
42+
<div>
43+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconTaskGeneral />} size="large" />
44+
<br />
45+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconTaskGeneral />} />
46+
<br />
47+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconTaskGeneral />} size="small" />
48+
<br />
49+
<br />
50+
51+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconAnnotation />} size="large" />
52+
<br />
53+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconAnnotation />} />
54+
<br />
55+
<Avatar id={1} name="Aaron Levie" badgeIcon={<IconAnnotation />} size="small" />
56+
<br />
57+
<br />
58+
59+
<Avatar
60+
badgeIcon={<IconVerified className="completed" title="Verified user" />}
61+
id={1}
62+
name="Aaron Levie"
63+
size="large"
64+
/>
65+
<br />
66+
<Avatar
67+
avatarUrl="data:image/jpeg;base64,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"
68+
badgeIcon={<IconVerified className="completed" title="Verified user" />}
69+
id={1}
70+
name="Aaron Levie"
71+
/>
72+
<br />
73+
<Avatar
74+
badgeIcon={<IconVerified className="completed" title="Verified user" />}
75+
id={1}
76+
name="Aaron Levie"
77+
size="small"
78+
/>
79+
</div>
80+
);
81+
3782
export const withMultipleAvatars = () => (
3883
<div>
3984
<Avatar id={1} name="Aaron Levie" />

src/components/avatar/Avatar.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export interface AvatarProps {
1717
* Required if "name" is not specified.
1818
*/
1919
avatarUrl?: string | null;
20+
/**
21+
* Icon React Element that will be shown as a badge in bottom right corner of Avatar.
22+
*
23+
* Will not be used if `shouldShowExternal` and `isExternal` is true, then GlobalBadge will be shown.
24+
*/
25+
badgeIcon?: React.ReactElement;
2026
/** classname to add to the container element. */
2127
className?: string;
2228
/** Users id */
@@ -35,14 +41,27 @@ export interface AvatarProps {
3541
size?: keyof typeof SIZES | '';
3642
}
3743

38-
function Avatar({ avatarUrl, className, name, id, isExternal, shouldShowExternal = false, size = '' }: AvatarProps) {
44+
function Avatar({
45+
avatarUrl,
46+
badgeIcon,
47+
className,
48+
name,
49+
id,
50+
isExternal,
51+
shouldShowExternal = false,
52+
size = '',
53+
}: AvatarProps) {
3954
const [hasImageErrored, setHasImageErrored] = React.useState<boolean>(false);
4055
const [prevAvatarUrl, setPrevAvatarUrl] = React.useState<AvatarProps['avatarUrl']>(null);
4156

4257
const classes = classNames([
4358
'avatar',
4459
className,
45-
{ [`avatar--${size}`]: size && SIZES[size], 'avatar--isExternal': shouldShowExternal && isExternal },
60+
{
61+
[`avatar--${size}`]: size && SIZES[size],
62+
'avatar--isExternal': shouldShowExternal && isExternal,
63+
'avatar--iconBadge': !!badgeIcon,
64+
},
4665
]);
4766

4867
// Reset hasImageErrored state when avatarUrl changes
@@ -67,13 +86,15 @@ function Avatar({ avatarUrl, className, name, id, isExternal, shouldShowExternal
6786
avatar = <UnknownUserAvatar className="avatar-icon" />;
6887
}
6988

89+
let badge = null;
90+
if (shouldShowExternal && isExternal) {
91+
badge = <GlobeBadge16 className="bdl-Avatar-externalBadge" />;
92+
} else if (badgeIcon) {
93+
badge = <div className="bdl-Avatar-badge bdl-Avatar-iconBadge">{badgeIcon}</div>;
94+
}
95+
7096
return (
71-
<Badgeable
72-
className={classes}
73-
bottomRight={
74-
shouldShowExternal && isExternal ? <GlobeBadge16 className="bdl-Avatar-externalBadge" /> : undefined
75-
}
76-
>
97+
<Badgeable className={classes} bottomRight={badge}>
7798
<span>{avatar}</span>
7899
</Badgeable>
79100
);

src/elements/content-sidebar/activity-feed/Avatar.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { GetAvatarUrlCallback } from '../../common/flowTypes';
99
import type { User } from '../../../common/types/core';
1010

1111
type Props = {
12+
badgeIcon?: React.Element<any>,
1213
className?: string,
1314
getAvatarUrl?: GetAvatarUrlCallback,
1415
user: User,
@@ -61,11 +62,13 @@ class Avatar extends React.PureComponent<Props, State> {
6162
}
6263

6364
render() {
64-
const { user, className }: Props = this.props;
65+
const { badgeIcon, className, user }: Props = this.props;
6566
const { avatarUrl }: State = this.state;
6667
const { id, name } = user;
6768

68-
return <AvatarComponent avatarUrl={avatarUrl} className={className} id={id} name={name} />;
69+
return (
70+
<AvatarComponent avatarUrl={avatarUrl} badgeIcon={badgeIcon} className={className} id={id} name={name} />
71+
);
6972
}
7073
}
7174

src/elements/content-sidebar/activity-feed/__tests__/__snapshots__/Avatar.test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exports[`elements/content-sidebar/ActivityFeed/Avatar should render the avatar w
1010

1111
exports[`elements/content-sidebar/ActivityFeed/Avatar should set the avatarUrl state from user prop 1`] = `
1212
<Badgeable
13+
bottomRight={null}
1314
className="avatar"
1415
>
1516
<span>

src/elements/content-sidebar/activity-feed/annotations/AnnotationActivity.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import type { Annotation, AnnotationPermission, FeedItemStatus } from '../../../
2323
import type { GetAvatarUrlCallback, GetProfileUrlCallback } from '../../../common/flowTypes';
2424
import type { SelectorItems, User } from '../../../../common/types/core';
2525

26+
import IconAnnotation from '../../../../icons/two-toned/IconAnnotation';
27+
2628
import './AnnotationActivity.scss';
2729

2830
type Props = {
@@ -138,8 +140,8 @@ const AnnotationActivity = ({
138140
'bcs-is-pending': isPending || error,
139141
})}
140142
>
141-
<Media.Figure>
142-
<Avatar getAvatarUrl={getAvatarUrl} user={createdByUser} />
143+
<Media.Figure className="bcs-AnnotationActivity-avatar">
144+
<Avatar getAvatarUrl={getAvatarUrl} user={createdByUser} badgeIcon={<IconAnnotation />} />
143145
</Media.Figure>
144146
<Media.Body>
145147
<div className="bcs-AnnotationActivity-headline">

src/elements/content-sidebar/activity-feed/annotations/AnnotationActivity.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import '../../../common/variables';
2+
@import '../../mixins';
23

34
.bcs-AnnotationActivity {
45
position: relative;
@@ -16,6 +17,10 @@
1617
}
1718
}
1819

20+
.bcs-AnnotationActivity-avatar {
21+
position: relative;
22+
}
23+
1924
.bcs-AnnotationActivity-headline {
2025
// Because the annotation menu is absolutely positioned,
2126
// this padding reserves that space so long headlines won't overlap with the menu

src/elements/content-sidebar/activity-feed/task-new/AvatarGroupAvatar.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ const AvatarGroupAvatar = React.memo<Props>(({ user, status, getAvatarUrl, class
4040
data-testid="avatar-group-avatar-container"
4141
{...rest}
4242
>
43-
<Avatar className="bcs-AvatarGroupAvatar-avatar" user={user} getAvatarUrl={getAvatarUrl} />
44-
<StatusIcon
45-
status={status}
46-
className={`bcs-AvatarGroupAvatar-statusIcon ${camelCase(status)}`}
47-
height={12}
48-
width={12}
49-
title={<FormattedMessage {...messages.taskAssignmentCompleted} />}
43+
<Avatar
44+
badgeIcon={
45+
<StatusIcon
46+
className={`${camelCase(status)}`}
47+
status={status}
48+
title={<FormattedMessage {...messages.taskAssignmentCompleted} />}
49+
/>
50+
}
51+
className="bcs-AvatarGroupAvatar-avatar"
52+
getAvatarUrl={getAvatarUrl}
53+
user={user}
5054
/>
5155
</div>
5256
));

src/elements/content-sidebar/activity-feed/task-new/AvatarGroupAvatar.scss

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@import '../../../common/variables';
2-
@import './mixins';
2+
@import '../../mixins';
33

44
$bcs-AvatarGroupAvatar-avatar-size: 32px;
55

@@ -16,7 +16,3 @@ $bcs-AvatarGroupAvatar-avatar-size: 32px;
1616
width: 100%;
1717
height: 100%;
1818
}
19-
20-
.bcs-AvatarGroupAvatar-statusIcon {
21-
@include avatar-badge-icon;
22-
}

0 commit comments

Comments
 (0)