Skip to content

Commit 07e6821

Browse files
Fix: Avatar overflow style mismatch (#368)
* fix: overflow avatar not matching design * chore: add example * refactor: move util function to utils * tests: add utils tests * chore: update comment * chore: import * refactor: props from element
1 parent 0c070b9 commit 07e6821

File tree

4 files changed

+114
-10
lines changed

4 files changed

+114
-10
lines changed

apps/www/src/app/examples/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,36 @@ const Page = () => {
976976
</Flex>
977977
</Flex>
978978

979+
<Text
980+
size="large"
981+
weight="medium"
982+
style={{ marginTop: "32px", marginBottom: "16px" }}>
983+
Avatar Examples
984+
</Text>
985+
986+
<Flex direction="column" gap={6}>
987+
<Flex direction="column" gap={3}>
988+
<AvatarGroup max={4}>
989+
<Tooltip message="JD">
990+
<Avatar radius="small" size={7} fallback="JD" color="indigo" />
991+
</Tooltip>
992+
<Tooltip message="AS">
993+
<Avatar radius="small" size={7} fallback="AS" color="mint" />
994+
</Tooltip>
995+
<Tooltip message="RK">
996+
<Avatar radius="small" size={7} fallback="RK" color="sky" />
997+
</Tooltip>
998+
<Tooltip message="PL">
999+
<Avatar radius="small" size={7} fallback="PL" color="purple" />
1000+
</Tooltip>
1001+
<Tooltip message="MN">
1002+
<Avatar radius="small" size={7} fallback="MN" color="pink" />
1003+
</Tooltip>
1004+
</AvatarGroup>
1005+
</Flex>
1006+
</Flex>
1007+
1008+
9791009
<Flex justify="center" style={{ marginTop: 40 }}>
9801010
<Button type="submit">Submit button</Button>
9811011
</Flex>

packages/raystack/v1/components/avatar/avatar.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import * as AvatarPrimitive from "@radix-ui/react-avatar";
2-
import { cva, VariantProps } from "class-variance-authority";
3-
import { clsx } from 'clsx';
41
import {
52
ComponentPropsWithoutRef,
63
ElementRef,
74
forwardRef,
8-
ReactNode,
5+
ReactNode
96
} from "react";
7+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
8+
import { cva, VariantProps } from "class-variance-authority";
9+
import { clsx } from 'clsx';
1010

11+
import {AVATAR_COLORS, getAvatarProps} from './utils'
1112
import { Box } from "../box";
1213
import styles from "./avatar.module.css";
13-
import {AVATAR_COLORS} from './utils'
1414

1515
const avatar = cva(styles.avatar, {
1616
variants: {
@@ -147,6 +147,9 @@ export const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(
147147
const avatars = max ? children.slice(0, max) : children;
148148
const count = max && children.length > max ? children.length - max : 0;
149149

150+
// Overflow avatar matches the first avatar styling
151+
const firstAvatarProps = getAvatarProps(avatars[0]);
152+
150153
return (
151154
<div
152155
ref={ref}
@@ -161,11 +164,11 @@ export const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(
161164
{count > 0 && (
162165
<div className={styles.avatarWrapper}>
163166
<Avatar
164-
size={avatars[0].props.size}
165-
radius={avatars[0].props.radius}
166-
variant={avatars[0].props.variant}
167+
size={firstAvatarProps.size}
168+
radius={firstAvatarProps.radius}
169+
variant={firstAvatarProps.variant}
167170
color='neutral'
168-
fallback={<span>+{count}</span>}
171+
fallback={`+${count}`}
169172
/>
170173
</div>
171174
)}

packages/raystack/v1/components/avatar/utils.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { getAvatarColor } from "./utils";
1+
import { getAvatarColor, getAvatarProps } from "./utils";
2+
import { Avatar } from "./avatar";
3+
import React from "react";
4+
25
describe("getAvatarColor", () => {
36
it("should return same color for the same name", () => {
47
const color = getAvatarColor("John Doe");
@@ -23,3 +26,47 @@ describe("getAvatarColor", () => {
2326
expect(color).toBe("grass");
2427
});
2528
});
29+
30+
describe("getAvatarProps", () => {
31+
it("should return props directly from Avatar component", () => {
32+
const props = {
33+
size: 5 as const,
34+
color: "indigo" as const,
35+
fallback: "GS",
36+
};
37+
const element = React.createElement(Avatar, props);
38+
const result = getAvatarProps(element);
39+
expect(result).toEqual(props);
40+
});
41+
42+
it("should return props from Avatar wrapped in another component", () => {
43+
const avatarProps = {
44+
size: 5 as const,
45+
color: "mint" as const,
46+
fallback: "GS",
47+
};
48+
const avatar = React.createElement(Avatar, avatarProps);
49+
const wrapper = React.createElement('div', {}, avatar);
50+
const result = getAvatarProps(wrapper);
51+
expect(result).toEqual(avatarProps);
52+
});
53+
54+
it("should return props from deeply nested Avatar", () => {
55+
const avatarProps = {
56+
size: 7 as const,
57+
color: "sky" as const,
58+
fallback: "GS",
59+
};
60+
const avatar = React.createElement(Avatar, avatarProps);
61+
const innerWrapper = React.createElement('div', {}, avatar);
62+
const outerWrapper = React.createElement('div', {}, innerWrapper);
63+
const result = getAvatarProps(outerWrapper);
64+
expect(result).toEqual(avatarProps);
65+
});
66+
67+
it("should return empty object when children is not a valid element", () => {
68+
const element = React.createElement('div', {}, 'text content');
69+
const result = getAvatarProps(element);
70+
expect(result).toEqual({});
71+
});
72+
});

packages/raystack/v1/components/avatar/utils.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { isValidElement, ReactElement } from "react";
2+
import { Avatar } from "./avatar";
3+
4+
import { AvatarProps } from "./avatar";
5+
16
export const COLORS = [
27
"indigo",
38
"orange",
@@ -21,3 +26,22 @@ export function getAvatarColor(str: string): AVATAR_COLORS {
2126
const index = hash % COLORS.length;
2227
return COLORS[index];
2328
}
29+
30+
/*
31+
* @desc Recursively get the avatar props even if it's
32+
* wrapped in another component like Tooltip, Flex, etc.
33+
*/
34+
export const getAvatarProps = (element: ReactElement): AvatarProps => {
35+
const { props } = element;
36+
37+
if (element.type === Avatar) {
38+
return props;
39+
}
40+
41+
if (props.children) {
42+
if (isValidElement(props.children)) {
43+
return getAvatarProps(props.children);
44+
}
45+
}
46+
return {};
47+
};

0 commit comments

Comments
 (0)