Skip to content

Commit dc410a7

Browse files
committed
Simplify Avatar usage
1 parent f079dcd commit dc410a7

File tree

30 files changed

+168
-366
lines changed

30 files changed

+168
-366
lines changed
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { Avatar, AvatarFallback, AvatarImage } from "./Avatar";
2+
import { Avatar } from "./Avatar";
33

44
const meta: Meta<typeof Avatar> = {
55
title: "components/Avatar",
66
component: Avatar,
77
args: {
8-
children: <AvatarImage src="https://github.com/kylecarbs.png" />,
8+
src: "https://github.com/kylecarbs.png",
99
},
1010
};
1111

@@ -26,55 +26,48 @@ export const IconLgSize: Story = {
2626
args: {
2727
size: "lg",
2828
variant: "icon",
29-
children: (
30-
<AvatarImage src="https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png" />
31-
),
29+
src: "https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png",
3230
},
3331
};
3432

3533
export const IconDefaultSize: Story = {
3634
args: {
3735
variant: "icon",
38-
children: (
39-
<AvatarImage src="https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png" />
40-
),
36+
src: "https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png",
4137
},
4238
};
4339

4440
export const IconSmSize: Story = {
4541
args: {
4642
variant: "icon",
4743
size: "sm",
48-
children: (
49-
<AvatarImage src="https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png" />
50-
),
44+
src: "https://em-content.zobj.net/source/apple/391/billed-cap_1f9e2.png",
5145
},
5246
};
5347

5448
export const NonSquaredIcon: Story = {
5549
args: {
5650
variant: "icon",
57-
children: <AvatarImage src="/icon/docker.png" />,
51+
src: "/icon/docker.png",
5852
},
5953
};
6054

6155
export const FallbackLgSize: Story = {
6256
args: {
6357
size: "lg",
64-
65-
children: <AvatarFallback>AR</AvatarFallback>,
58+
fallback: "Adriana Rodrigues",
6659
},
6760
};
6861

6962
export const FallbackDefaultSize: Story = {
7063
args: {
71-
children: <AvatarFallback>AR</AvatarFallback>,
64+
fallback: "Adriana Rodrigues",
7265
},
7366
};
7467

7568
export const FallbackSmSize: Story = {
7669
args: {
7770
size: "sm",
78-
children: <AvatarFallback>AR</AvatarFallback>,
71+
fallback: "Adriana Rodrigues",
7972
},
8073
};

site/src/components/Avatar/Avatar.tsx

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { useTheme } from "@emotion/react";
2-
import * as AvatarPrimitive from "@radix-ui/react-avatar";
3-
import { type VariantProps, cva } from "class-variance-authority";
41
/**
52
* Copied from shadc/ui on 12/16/2024
63
* @see {@link https://ui.shadcn.com/docs/components/avatar}
74
*
85
* This component was updated to support the variants and match the styles from
96
* the Figma design:
107
* @see {@link https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=711-383&t=xqxOSUk48GvDsjGK-0}
8+
*
9+
* It was also simplified to make usage easier and reduce boilerplate.
10+
* @see {@link https://github.com/coder/coder/pull/15930#issuecomment-2552292440}
1111
*/
12+
13+
import { useTheme } from "@emotion/react";
14+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
15+
import { type VariantProps, cva } from "class-variance-authority";
1216
import * as React from "react";
1317
import { getExternalImageStylesFromUrl } from "theme/externalImages";
1418
import { cn } from "utils/cn";
@@ -50,56 +54,38 @@ const avatarVariants = cva(
5054
},
5155
);
5256

53-
export interface AvatarProps
54-
extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
55-
VariantProps<typeof avatarVariants> {}
57+
export type AvatarProps = Omit<AvatarPrimitive.AvatarProps, "children"> &
58+
VariantProps<typeof avatarVariants> & {
59+
src?: string;
60+
alt?: string;
61+
fallback?: string;
62+
};
5663

5764
const Avatar = React.forwardRef<
5865
React.ElementRef<typeof AvatarPrimitive.Root>,
5966
AvatarProps
60-
>(({ className, size, variant, ...props }, ref) => (
61-
<AvatarPrimitive.Root
62-
ref={ref}
63-
className={cn(avatarVariants({ size, variant, className }))}
64-
{...props}
65-
/>
66-
));
67-
Avatar.displayName = AvatarPrimitive.Root.displayName;
68-
69-
const AvatarImage = React.forwardRef<
70-
React.ElementRef<typeof AvatarPrimitive.Image>,
71-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
72-
>(({ className, ...props }, ref) => {
67+
>(({ className, size, variant, alt, fallback, ...props }, ref) => {
7368
const theme = useTheme();
7469

7570
return (
76-
<AvatarPrimitive.Image
71+
<AvatarPrimitive.Root
7772
ref={ref}
78-
className={cn("aspect-square h-full w-full object-contain", className)}
79-
css={getExternalImageStylesFromUrl(theme.externalImages, props.src)}
73+
className={cn(avatarVariants({ size, variant, className }))}
8074
{...props}
81-
/>
75+
>
76+
<AvatarPrimitive.Image
77+
className="aspect-square h-full w-full object-contain"
78+
css={getExternalImageStylesFromUrl(theme.externalImages, props.src)}
79+
alt={alt}
80+
/>
81+
{fallback && (
82+
<AvatarPrimitive.Fallback className="flex h-full w-full items-center justify-center rounded-full">
83+
{fallback.charAt(0).toUpperCase()}
84+
</AvatarPrimitive.Fallback>
85+
)}
86+
</AvatarPrimitive.Root>
8287
);
8388
});
84-
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
85-
86-
const AvatarFallback = React.forwardRef<
87-
React.ElementRef<typeof AvatarPrimitive.Fallback>,
88-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
89-
>(({ className, ...props }, ref) => (
90-
<AvatarPrimitive.Fallback
91-
ref={ref}
92-
className={cn(
93-
"flex h-full w-full items-center justify-center rounded-full",
94-
className,
95-
)}
96-
{...props}
97-
/>
98-
));
99-
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
100-
101-
function avatarLetter(str: string): string {
102-
return str.charAt(0).toUpperCase();
103-
}
89+
Avatar.displayName = AvatarPrimitive.Root.displayName;
10490

105-
export { Avatar, AvatarImage, AvatarFallback, avatarLetter };
91+
export { Avatar };

site/src/components/Avatar/AvatarCard.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { type CSSObject, useTheme } from "@emotion/react";
2-
import {
3-
Avatar,
4-
AvatarFallback,
5-
AvatarImage,
6-
avatarLetter,
7-
} from "components/Avatar/Avatar";
2+
import { Avatar } from "components/Avatar/Avatar";
83
import type { FC, ReactNode } from "react";
94

105
type AvatarCardProps = {
116
header: string;
127
imgUrl: string;
138
altText: string;
14-
159
subtitle?: ReactNode;
1610
maxWidth?: number | "none";
1711
};
@@ -75,10 +69,7 @@ export const AvatarCard: FC<AvatarCardProps> = ({
7569
)}
7670
</div>
7771

78-
<Avatar size="lg">
79-
<AvatarImage src={imgUrl} alt={altText} />
80-
<AvatarFallback>{avatarLetter(header)}</AvatarFallback>
81-
</Avatar>
72+
<Avatar size="lg" src={imgUrl} alt={altText} fallback={header} />
8273
</div>
8374
);
8475
};

site/src/components/Avatar/AvatarData.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { useTheme } from "@emotion/react";
2-
import {
3-
Avatar,
4-
AvatarFallback,
5-
AvatarImage,
6-
avatarLetter,
7-
} from "components/Avatar/Avatar";
2+
import { Avatar } from "components/Avatar/Avatar";
83
import { Stack } from "components/Stack/Stack";
94
import type { FC, ReactNode } from "react";
105

@@ -34,14 +29,10 @@ export const AvatarData: FC<AvatarDataProps> = ({
3429
const theme = useTheme();
3530
if (!avatar) {
3631
avatar = (
37-
<Avatar>
38-
<AvatarImage src={src} />
39-
<AvatarFallback>
40-
{avatarLetter(
41-
(typeof title === "string" ? title : imgFallbackText) || "-",
42-
)}
43-
</AvatarFallback>
44-
</Avatar>
32+
<Avatar
33+
src={src}
34+
fallback={(typeof title === "string" ? title : imgFallbackText) || "-"}
35+
/>
4536
);
4637
}
4738

site/src/components/FullPageLayout/Topbar.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { css } from "@emotion/css";
22
import { useTheme } from "@emotion/react";
33
import Button, { type ButtonProps } from "@mui/material/Button";
44
import IconButton, { type IconButtonProps } from "@mui/material/IconButton";
5-
import { Avatar, AvatarImage } from "components/Avatar/Avatar";
5+
import { Avatar } from "components/Avatar/Avatar";
66
import {
77
type FC,
88
type ForwardedRef,
@@ -94,11 +94,7 @@ export const TopbarDivider: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
9494
};
9595

9696
export const TopbarAvatar: FC<{ src: string }> = ({ src }) => {
97-
return (
98-
<Avatar variant="icon" size="sm">
99-
<AvatarImage src={src} />
100-
</Avatar>
101-
);
97+
return <Avatar variant="icon" size="sm" src={src} />;
10298
};
10399

104100
type TopbarIconProps = HTMLAttributes<HTMLOrSVGElement>;

site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import TextField from "@mui/material/TextField";
55
import { checkAuthorization } from "api/queries/authCheck";
66
import { organizations } from "api/queries/organizations";
77
import type { AuthorizationCheck, Organization } from "api/typesGenerated";
8-
import {
9-
Avatar,
10-
AvatarFallback,
11-
AvatarImage,
12-
avatarLetter,
13-
} from "components/Avatar/Avatar";
8+
import { Avatar } from "components/Avatar/Avatar";
149
import { AvatarData } from "components/Avatar/AvatarData";
1510
import { useDebouncedFunction } from "hooks/debounce";
1611
import {
@@ -137,10 +132,7 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
137132
...params.InputProps,
138133
onChange: debouncedInputOnChange,
139134
startAdornment: value && (
140-
<Avatar size="sm">
141-
<AvatarImage src={value.icon} />
142-
<AvatarFallback>{avatarLetter(value.name)}</AvatarFallback>
143-
</Avatar>
135+
<Avatar size="sm" src={value.icon} fallback={value.name} />
144136
),
145137
endAdornment: (
146138
<>

site/src/components/UserAutocomplete/UserAutocomplete.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import { getErrorMessage } from "api/errors";
66
import { organizationMembers } from "api/queries/organizations";
77
import { users } from "api/queries/users";
88
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
9-
import {
10-
Avatar,
11-
AvatarFallback,
12-
AvatarImage,
13-
avatarLetter,
14-
} from "components/Avatar/Avatar";
9+
import { Avatar } from "components/Avatar/Avatar";
1510
import { AvatarData } from "components/Avatar/AvatarData";
1611
import { useDebouncedFunction } from "hooks/debounce";
1712
import {
@@ -175,10 +170,11 @@ const InnerAutocomplete = <T extends SelectedUser>({
175170
...params.InputProps,
176171
onChange: debouncedInputOnChange,
177172
startAdornment: value && (
178-
<Avatar size="sm">
179-
<AvatarImage src={value.avatar_url} />
180-
<AvatarFallback>{avatarLetter(value.username)}</AvatarFallback>
181-
</Avatar>
173+
<Avatar
174+
size="sm"
175+
src={value.avatar_url}
176+
fallback={value.username}
177+
/>
182178
),
183179
endAdornment: (
184180
<>
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
Avatar,
3-
AvatarFallback,
4-
AvatarImage,
5-
avatarLetter,
6-
} from "components/Avatar/Avatar";
1+
import { Avatar } from "components/Avatar/Avatar";
72
import type { FC } from "react";
83

94
export interface GroupAvatarProps {
@@ -12,10 +7,5 @@ export interface GroupAvatarProps {
127
}
138

149
export const GroupAvatar: FC<GroupAvatarProps> = ({ name, avatarURL }) => {
15-
return (
16-
<Avatar>
17-
<AvatarImage src={avatarURL} />
18-
<AvatarFallback>{avatarLetter(name)}</AvatarFallback>
19-
</Avatar>
20-
);
10+
return <Avatar src={avatarURL} fallback={name} />;
2111
};

site/src/modules/management/OrganizationSidebarView.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { cx } from "@emotion/css";
22
import AddIcon from "@mui/icons-material/Add";
33
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
4-
import {
5-
Avatar,
6-
AvatarFallback,
7-
AvatarImage,
8-
avatarLetter,
9-
} from "components/Avatar/Avatar";
4+
import { Avatar } from "components/Avatar/Avatar";
105
import { Loader } from "components/Loader/Loader";
116
import {
127
Sidebar as BaseSidebar,
@@ -133,12 +128,11 @@ const OrganizationSettingsNavigation: FC<
133128
active={active}
134129
href={urlForSubpage(organization.name)}
135130
icon={
136-
<Avatar variant="icon">
137-
<AvatarImage src={organization.icon} />
138-
<AvatarFallback>
139-
{avatarLetter(organization.display_name)}
140-
</AvatarFallback>
141-
</Avatar>
131+
<Avatar
132+
variant="icon"
133+
src={organization.icon}
134+
fallback={organization.display_name}
135+
/>
142136
}
143137
>
144138
{organization.display_name}
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import type { WorkspaceResource } from "api/typesGenerated";
2-
import {
3-
Avatar,
4-
AvatarFallback,
5-
AvatarImage,
6-
avatarLetter,
7-
} from "components/Avatar/Avatar";
2+
import { Avatar } from "components/Avatar/Avatar";
83
import type { FC } from "react";
94
import { getResourceIconPath } from "utils/workspace";
105

@@ -13,10 +8,5 @@ export type ResourceAvatarProps = { resource: WorkspaceResource };
138
export const ResourceAvatar: FC<ResourceAvatarProps> = ({ resource }) => {
149
const avatarSrc = resource.icon || getResourceIconPath(resource.type);
1510

16-
return (
17-
<Avatar variant="icon">
18-
<AvatarImage src={avatarSrc} />
19-
<AvatarFallback>{avatarLetter(resource.name)}</AvatarFallback>
20-
</Avatar>
21-
);
11+
return <Avatar variant="icon" src={avatarSrc} fallback={resource.name} />;
2212
};

0 commit comments

Comments
 (0)