Skip to content
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
1 change: 1 addition & 0 deletions apps/www/src/content/docs/components/avatar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: Avatar
description: An image element with a fallback for representing the user.
source: packages/raystack/components/avatar
tag: new
---

import {
Expand Down
10 changes: 8 additions & 2 deletions apps/www/src/content/docs/components/avatar/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ export interface AvatarProps {
| 'crimson'
| 'gold';

/** Boolean to merge props onto child element */
asChild?: boolean;
/**
* Allows you to replace the component's HTML element with a different tag,
* or compose it with another component. Accepts a ReactElement or a function
* that returns the element to render.
*
* @remarks `ReactElement | function`
*/
render?: React.ReactElement;

/** Additional CSS class names */
className?: string;
Expand Down
25 changes: 17 additions & 8 deletions packages/raystack/components/avatar/__tests__/avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { Avatar, AvatarGroup } from '../avatar';
import styles from '../avatar.module.css';
import { getAvatarColor } from '../utils';
Expand Down Expand Up @@ -312,6 +312,8 @@ describe('Avatar', () => {

class MockImage extends EventTarget {
_src: string = '';
_complete: boolean = false;
onload: (() => void) | null = null;

constructor() {
super();
Expand All @@ -327,20 +329,27 @@ class MockImage extends EventTarget {
return;
}
this._src = src;
this.onSrcChange();
// Simulate async image loading
setTimeout(() => {
this._complete = true;
// Call onload callback if set
if (this.onload) {
this.onload();
}
// Also dispatch the event
this.dispatchEvent(new Event('load'));
}, 0);
}

get complete() {
return !this.src;
return this._complete;
}

get naturalWidth() {
return this.complete ? 300 : 0;
return this._complete ? 300 : 0;
}

onSrcChange() {
setTimeout(() => {
this.dispatchEvent(new Event('load'));
}, 100);
get naturalHeight() {
return this._complete ? 300 : 0;
}
}
54 changes: 26 additions & 28 deletions packages/raystack/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { VariantProps, cva, cx } from 'class-variance-authority';
import { Avatar as AvatarPrimitive } from 'radix-ui';
import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
import { cva, cx, VariantProps } from 'class-variance-authority';
import {
ComponentPropsWithoutRef,
ElementRef,
ReactElement,
ReactNode,
forwardRef,
isValidElement
isValidElement,
ReactElement,
ReactNode
} from 'react';
import { Box } from '../box';
import styles from './avatar.module.css';
import { AVATAR_COLORS } from './utils';

Expand Down Expand Up @@ -133,54 +131,54 @@ const image = cva(styles.image);
* @desc Recursively get the avatar props even if it's
* wrapped in another component like Tooltip, Flex, etc.
*/
export const getAvatarProps = (element: ReactElement): AvatarProps => {
const { props } = element;
export const getAvatarProps = (
element: ReactElement<AvatarProps>
): AvatarProps => {
const props = element.props as AvatarProps & { children?: ReactNode };

if (element.type === Avatar) {
return props;
}

if (props.children) {
if (isValidElement(props.children)) {
if (isValidElement<AvatarProps>(props.children)) {
return getAvatarProps(props.children);
}
}
return {};
};

export interface AvatarProps
extends ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
extends AvatarPrimitive.Root.Props,
VariantProps<typeof avatar> {
size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
src?: string;
alt?: string;
fallback?: ReactNode;
variant?: 'solid' | 'soft';
color?: AVATAR_COLORS;
asChild?: boolean;
className?: string;
}

const AvatarRoot = forwardRef<
ElementRef<typeof AvatarPrimitive.Root>,
AvatarProps
>(
const AvatarRoot = forwardRef<HTMLSpanElement, AvatarProps>(
(
{ className, alt, src, fallback, size, radius, variant, color, ...props },
ref
) => (
<Box className={styles.imageWrapper}>
<AvatarPrimitive.Root
ref={ref}
className={cx(avatar({ size, radius, variant, color }), className)}
{...props}
>
<AvatarPrimitive.Image className={image()} src={src} alt={alt} />
<AvatarPrimitive.Fallback className={styles.fallback}>
{fallback}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
</Box>
<AvatarPrimitive.Root
ref={ref}
className={cx(
styles.imageWrapper,
avatar({ size, radius, variant, color }),
className
)}
{...props}
>
<AvatarPrimitive.Image className={image()} src={src} alt={alt} />
<AvatarPrimitive.Fallback className={styles.fallback}>
{fallback}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
)
);

Expand Down
4 changes: 2 additions & 2 deletions packages/raystack/components/avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Avatar, AvatarGroup } from "./avatar";
export { getAvatarColor } from "./utils";
export { Avatar, AvatarGroup } from './avatar';
export { getAvatarColor } from './utils';