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
46 changes: 10 additions & 36 deletions src/components/ui/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,20 @@
import React from 'react';

Check warning on line 1 in src/components/ui/Avatar/Avatar.tsx

View workflow job for this annotation

GitHub Actions / lint

'React' is defined but never used
import { clsx } from 'clsx';

Check warning on line 2 in src/components/ui/Avatar/Avatar.tsx

View workflow job for this annotation

GitHub Actions / lint

'clsx' is defined but never used
import AvatarPrimitive from '~/core/primitives/Avatar';
import { useCreateDataAttribute, useComposeAttributes, useCreateDataAccentColorAttribute } from '~/core/hooks/createDataAttribute';

const COMPONENT_NAME = 'Avatar';

export type AvatarProps = {
import AvatarRoot from './fragments/AvatarRoot';
import AvatarImage from './fragments/AvatarImage';
import AvatarFallback from './fragments/AvatarFallback';

customRootClass?: string,
fallback?: string,
className?: string,
variant?: string;
size?:string;
src?: string,
alt?: string,
color?:string,
asChild?: boolean,
props?: Record<string, any>[]
}
const COMPONENT_NAME = 'Avatar';

const Avatar = ({ customRootClass = '', fallback, className, src, alt, variant = '', size = '', asChild = false, color = '', ...props }: AvatarProps) => {
const dataAttributes = useCreateDataAttribute('avatar', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
const composedAttributes = useComposeAttributes(dataAttributes(), accentAttributes());
return (
<AvatarPrimitive.Root customRootClass={customRootClass} asChild={asChild} {...composedAttributes()}>
<AvatarPrimitive.Image
src={src}
alt={alt}
className={clsx(className)}
{...props}
/>
<AvatarPrimitive.Fallback color={color}>
{fallback}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
);
const Avatar = () => {
console.warn('Direct usage of Avatar is not supported. Please use Avatar.Root, Avatar.Image, Avatar.Fallback instead.');
return null;
};

Avatar.displayName = COMPONENT_NAME;
Avatar.Root = AvatarPrimitive.Root;
Avatar.Image = AvatarPrimitive.Image;
Avatar.Fallback = AvatarPrimitive.Fallback;
Avatar.Root = AvatarRoot;
Avatar.Image = AvatarImage;
Avatar.Fallback = AvatarFallback;

export default Avatar;
14 changes: 14 additions & 0 deletions src/components/ui/Avatar/contexts/AvatarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext } from 'react';

type AvatarContextType = {
size?: string;
variant?: string;
color?: string;
rootClass?: string;
}
export const AvatarContext = createContext<AvatarContextType>({
size: '',
variant: '',
color: '',
rootClass: ''
} as AvatarContextType);
14 changes: 14 additions & 0 deletions src/components/ui/Avatar/fragments/AvatarFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import React, { useContext } from 'react';

import AvatarPrimitiveFallback, { AvatarPrimitiveFallbackProps } from '~/core/primitives/Avatar/fragments/AvatarPrimitiveFallback';

Check warning on line 5 in src/components/ui/Avatar/fragments/AvatarFallback.tsx

View workflow job for this annotation

GitHub Actions / lint

'AvatarPrimitiveFallbackProps' is defined but never used
import { AvatarContext } from '../contexts/AvatarContext';
import { clsx } from 'clsx';

const AvatarFallback = ({ children, ...props }: AvatarPrimitiveFallbackProps) => {
const { rootClass } = useContext(AvatarContext);
return <AvatarPrimitiveFallback className={clsx(`${rootClass}-fallback`)} {...props}>{children}</AvatarPrimitiveFallback>;
};

export default AvatarFallback;
18 changes: 18 additions & 0 deletions src/components/ui/Avatar/fragments/AvatarImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import React, { useContext } from 'react';
import AvatarPrimitiveImage, { AvatarRootImageProps } from '~/core/primitives/Avatar/fragments/AvatarPrimitiveImage';
import { AvatarContext } from '../contexts/AvatarContext';
import { clsx } from 'clsx';

type AvatarImageProps = AvatarRootImageProps & {
src?: string;
alt?: string;
}

const AvatarImage = ({ src = '', alt = '', children, ...props }: AvatarImageProps) => {
const { rootClass } = useContext(AvatarContext);
return <AvatarPrimitiveImage className={clsx(`${rootClass}-image`)} src={src} alt={alt} {...props} />;
};

export default AvatarImage;
30 changes: 30 additions & 0 deletions src/components/ui/Avatar/fragments/AvatarRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
import React from 'react';
import AvatarPrimitiveRoot, { AvatarPrimitiveRootProps } from '~/core/primitives/Avatar/fragments/AvatarPrimitiveRoot';
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core';
import { AvatarContext } from '../contexts/AvatarContext';
import { useCreateDataAttribute, useComposeAttributes, useCreateDataAccentColorAttribute } from '~/core/hooks/createDataAttribute';
const COMPONENT_NAME = 'Avatar';

export type AvatarRootProps = AvatarPrimitiveRootProps & {
customRootClass?: string;
className?: string;
size?: string;
variant?: string;
color?: string;
}

const AvatarRoot = ({ children, customRootClass = '', className = '', size = '', variant = '', color = '', ...props }: AvatarRootProps) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);

const dataAttributes = useCreateDataAttribute('avatar', { variant, size });
const accentAttributes = useCreateDataAccentColorAttribute(color);
const composedAttributes = useComposeAttributes(dataAttributes(), accentAttributes());

return <AvatarContext.Provider value={{ size, variant, color, rootClass }}>
<AvatarPrimitiveRoot className={clsx(rootClass, className)} {...composedAttributes()} {...props} >{children}</AvatarPrimitiveRoot>
</AvatarContext.Provider>;
};

export default AvatarRoot;
35 changes: 34 additions & 1 deletion src/components/ui/Avatar/stories/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@ export default {
title: 'Components/Avatar',
component: Avatar,
render: (args: JSX.IntrinsicAttributes & AvatarProps) => <SandboxEditor>
<Avatar {...args} />
<Avatar.Root color={args.color} size={args.size} variant={args.variant} customRootClass={args.customRootClass}>
<Avatar.Image src={args.src} alt={args.alt} />
<Avatar.Fallback>{args.fallback}</Avatar.Fallback>
</Avatar.Root>
</SandboxEditor>
};

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const withSrc = {
args: {
src: avatarImage1,
fallback: 'PK',
color: 'blue',
size: 'small',
variant: 'outline'
}
};

export const withCustomRootClass = {
args: {
customRootClass: 'acme-corp',
src: avatarImage1,
fallback: 'PK'
}
Expand Down Expand Up @@ -48,3 +62,22 @@ export const withColor = {
color: 'blue'
}
};

export const withSize = {
args: {
fallback: 'RU',
size: 'small'
}
};

const WithoutImgTemplate = () => {
return <SandboxEditor>
<Avatar.Root size='small' variant='outline'>
<Avatar.Fallback>PK</Avatar.Fallback>
</Avatar.Root>
</SandboxEditor>;
};

export const withoutImg = {
render: (args: JSX.IntrinsicAttributes & AvatarProps) => <WithoutImgTemplate />
};
30 changes: 22 additions & 8 deletions src/components/ui/Avatar/tests/Avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,54 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import Avatar from '../Avatar';

const Component = () => {
return <Avatar.Root color='blue' size='small' variant='outline'>
<Avatar.Image src='https://i.pravatar.cc/300' alt='avatar' />
<Avatar.Fallback>RU</Avatar.Fallback>
</Avatar.Root>;
};

const FallbackComponent = () => {
return <Avatar.Root color='blue' size='small' variant='outline'>
<Avatar.Fallback>RU</Avatar.Fallback>
</Avatar.Root>;
};

describe('Avatar', () => {
test('renders Avatar component', () => {
render(<Avatar fallback="RU" />);
render(<Component />);
expect(screen.getByText('RU')).toBeInTheDocument();
});

test('renders img tag with valid src', () => {
const url = 'https://i.pravatar.cc/300';
render(<Avatar src={url} fallback="RU" />);
render(<Component />);
const image = screen.getByRole('img');
// check if image has url as src
expect(image).toHaveAttribute('src', url);
});

test('renders fallback when src is not provided', async() => {
render(<Avatar fallback="RU" />);
render(<Component />);
expect(screen.getByText('RU')).toBeInTheDocument();
});

test('renders avatar with the given variant', () => {
render(<Avatar fallback="RU" variant='outline'/>);
render(<FallbackComponent />);
const fallback = screen.getByText('RU');
expect(fallback.parentElement).toHaveAttribute('data-avatar-variant', 'outline');
});

test('renders avatar with the given size', () => {
render(<Avatar fallback="RU"size='small'/>);
render(<FallbackComponent />);
const fallback = screen.getByText('RU');
expect(fallback.parentElement).toHaveAttribute('data-avatar-size', 'small');
});

test('renders color for fallback when src is not provided', async() => {
render(<Avatar fallback="RU" color='blue'/>);
expect(screen.getByText('RU')).toHaveAttribute('data-rad-ui-accent-color', 'blue');
test('renders avatar with the given color', () => {
render(<FallbackComponent />);
const fallback = screen.getByText('RU');
expect(fallback.parentElement).toHaveAttribute('data-rad-ui-accent-color', 'blue');
});

test('renders avatar with the given asChild', () => {
Expand Down
40 changes: 9 additions & 31 deletions src/components/ui/AvatarGroup/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import React from 'react';
import { clsx } from 'clsx';
import AvatarGroupRoot from './fragments/AvatarGroupRoot';
import AvatarPrimitiveRoot from '~/core/primitives/Avatar/fragments/AvatarPrimitiveRoot';
import AvatarPrimitiveFallback from '~/core/primitives/Avatar/fragments/AvatarPrimitiveFallback';
import AvatarPrimitiveImage from '~/core/primitives/Avatar/fragments/AvatarPrimitiveImage';
import AvatarGroupItem from './fragments/AvatarGroupItem';
import AvatarGroupAvatar from './fragments/AvatarGroupAvatar';
import AvatarGroupFallback from './fragments/AvatarGroupFallback';

const COMPONENT_NAME = 'AvatarGroup';

// contexts

export type AvatarGroupProps = {
avatars: { fallback: string, src: string, alt: string }[];
size: 'sm' | 'md' | 'lg';
customRootClass?: string;
className?: string;
color?:string;
props?: Record<string, any>;
}

const AvatarGroup = ({ avatars = [], size, customRootClass = '', className, color, ...props }: AvatarGroupProps) => {
return <AvatarGroupRoot customRootClass={customRootClass} className={clsx(className)} {...props} >
{avatars.map((avatar, index) => (
<AvatarPrimitiveRoot key={index}>
<AvatarPrimitiveImage src={avatar.src} alt={avatar.alt} />
<AvatarPrimitiveFallback color={color}>{avatar.fallback}</AvatarPrimitiveFallback>
</AvatarPrimitiveRoot>
))}
</AvatarGroupRoot>;
const AvatarGroup = () => {
console.warn('Direct usage of AvatarGroup is not supported. Please use AvatarGroup.Root, AvatarGroup.Item, AvatarGroup.Avatar, AvatarGroup.Fallback instead.');
return null;
};

AvatarGroup.displayName = COMPONENT_NAME;
AvatarGroup.Root = AvatarGroupRoot;
AvatarGroup.AvatarRoot = AvatarPrimitiveRoot;
AvatarGroup.AvatarImage = AvatarPrimitiveImage;
AvatarGroup.AvatarFallback = AvatarPrimitiveFallback;
AvatarGroup.Item = AvatarGroupItem;
AvatarGroup.Avatar = AvatarGroupAvatar;
AvatarGroup.Fallback = AvatarGroupFallback;
Comment on lines +12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

API breaking change detected.

The component API has changed significantly. The previous API used AvatarGroup.AvatarRoot, AvatarGroup.AvatarImage, and AvatarGroup.AvatarFallback (as seen in the relevant code snippet), while the new API exports AvatarGroup.Item, AvatarGroup.Avatar, and AvatarGroup.Fallback.

This change will require updates to existing code that uses the AvatarGroup component.

Given this breaking change:

  1. Consider documenting a migration path for existing users
  2. Consider adding a temporary backwards compatibility layer
  3. Ensure that all usages in the codebase have been updated

🏁 Script executed:

#!/bin/bash
# Find usages of the old AvatarGroup API in the codebase
echo "Searching for usages of old AvatarGroup.AvatarRoot API..."
rg "AvatarGroup\.AvatarRoot" -t tsx -t jsx

echo "Searching for usages of old AvatarGroup.AvatarImage API..."
rg "AvatarGroup\.AvatarImage" -t tsx -t jsx

echo "Searching for usages of old AvatarGroup.AvatarFallback API..."
rg "AvatarGroup\.AvatarFallback" -t tsx -t jsx

Length of output: 608


🏁 Script executed:

#!/bin/bash
# Re-run searches for old AvatarGroup API without relying on unknown file types
echo "Searching for usages of old AvatarGroup.AvatarRoot API..."
rg "AvatarGroup\.AvatarRoot" -g "*.tsx" -g "*.jsx"

echo "Searching for usages of old AvatarGroup.AvatarImage API..."
rg "AvatarGroup\.AvatarImage" -g "*.tsx" -g "*.jsx"

echo "Searching for usages of old AvatarGroup.AvatarFallback API..."
rg "AvatarGroup\.AvatarFallback" -g "*.tsx" -g "*.jsx"

Length of output: 982


Update documentation and migration support for AvatarGroup API changes

The documentation still references the old API in docs/app/docs/components/avatar-group/docs/avatarGroup_anatomy.tsx. Please:

  • Replace old component names:
    - <AvatarGroup.AvatarRoot>
    + <AvatarGroup.Item>- <AvatarGroup.AvatarImage />
    + <AvatarGroup.Avatar />- <AvatarGroup.AvatarFallback />
    + <AvatarGroup.Fallback />
  • Add or update a migration guide outlining the mapping from the old API (AvatarRoot, AvatarImage, AvatarFallback) to the new one (Item, Avatar, Fallback).
  • (Optional) Introduce a temporary backwards-compatibility layer that aliases the old exports to the new ones with deprecation warnings, to ease the transition for existing users.

Committable suggestion skipped: line range outside the PR's diff.


export default AvatarGroup;
7 changes: 7 additions & 0 deletions src/components/ui/AvatarGroup/contexts/AvatarGroupContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react';

export const AvatarGroupContext = createContext({
rootClass: '',
size: '',
variant: ''
});
10 changes: 10 additions & 0 deletions src/components/ui/AvatarGroup/fragments/AvatarGroupAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client';

import React from 'react';
import AvatarPrimitiveImage from '~/core/primitives/Avatar/fragments/AvatarPrimitiveImage';

const AvatarGroupAvatar = ({ src, alt }: { src: string, alt: string }) => {
return <AvatarPrimitiveImage src={src} alt={alt} />;
};

export default AvatarGroupAvatar;
14 changes: 14 additions & 0 deletions src/components/ui/AvatarGroup/fragments/AvatarGroupFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import React, { useContext } from 'react';
import AvatarPrimitiveFallback from '~/core/primitives/Avatar/fragments/AvatarPrimitiveFallback';
import { AvatarGroupContext } from '../contexts/AvatarGroupContext';

const AvatarGroupFallback = ({ children }: { fallback?: string, children: React.ReactNode }) => {
const { rootClass } = useContext(AvatarGroupContext);
return <AvatarPrimitiveFallback className={`${rootClass}-fallback`}>
{children}
</AvatarPrimitiveFallback>;
};

export default AvatarGroupFallback;
19 changes: 19 additions & 0 deletions src/components/ui/AvatarGroup/fragments/AvatarGroupItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import React, { useContext } from 'react';
import AvatarPrimitiveRoot from '~/core/primitives/Avatar/fragments/AvatarPrimitiveRoot';
import { AvatarGroupContext } from '../contexts/AvatarGroupContext';
import { useCreateDataAccentColorAttribute, useComposeAttributes } from '~/core/hooks/createDataAttribute';

const AvatarGroupItem = ({ color = '', children }: { color?: string, children: React.ReactNode }) => {
const { rootClass } = useContext(AvatarGroupContext);

const accentAttributes = useCreateDataAccentColorAttribute(color);
const composedAttributes = useComposeAttributes(accentAttributes());

return <AvatarPrimitiveRoot className={`${rootClass}-item`} {...composedAttributes()}>
{children}
</AvatarPrimitiveRoot>;
};

export default AvatarGroupItem;
22 changes: 16 additions & 6 deletions src/components/ui/AvatarGroup/fragments/AvatarGroupRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import React from 'react';
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core/customClassSwitcher';

import { AvatarGroupContext } from '../contexts/AvatarGroupContext';
import { useCreateDataAttribute, useComposeAttributes, useCreateDataAccentColorAttribute } from '~/core/hooks/createDataAttribute';

Check warning on line 5 in src/components/ui/AvatarGroup/fragments/AvatarGroupRoot.tsx

View workflow job for this annotation

GitHub Actions / lint

'useCreateDataAccentColorAttribute' is defined but never used
type AvatarGroupRootProps = {
customRootClass?: string | '';
size?: string;
variant?: string;
children: React.ReactNode;
className?: string;
}

const AvatarGroupRoot = ({ customRootClass = '', children, className, ...props }: AvatarGroupRootProps) => {
const rootClass = customClassSwitcher(customRootClass, 'AvatarGroup');
const COMPONENT_NAME = 'AvatarGroup';

const AvatarGroupRoot = ({ customRootClass = '', size = '', variant = '', children, className, ...props }: AvatarGroupRootProps) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const dataAttributes = useCreateDataAttribute('avatar', { variant, size });
const composedAttributes = useComposeAttributes(dataAttributes());

return (
<div className={clsx(rootClass, className)} {...props}>
{children}
</div>
<AvatarGroupContext.Provider value={{ size, variant, rootClass }}>
<div className={clsx(rootClass, className)} {...composedAttributes()} {...props}>
{children}
</div>
</AvatarGroupContext.Provider>
);
};

Expand Down
Loading
Loading