diff --git a/.yarn/versions/48669fbc.yml b/.yarn/versions/48669fbc.yml
new file mode 100644
index 000000000..834dea45c
--- /dev/null
+++ b/.yarn/versions/48669fbc.yml
@@ -0,0 +1,3 @@
+releases:
+ "@radix-ui/react-avatar": minor
+ primitives: patch
diff --git a/packages/react/avatar/src/Avatar.stories.tsx b/packages/react/avatar/src/Avatar.stories.tsx
index 0ee66a2bf..c1c5d0fc4 100644
--- a/packages/react/avatar/src/Avatar.stories.tsx
+++ b/packages/react/avatar/src/Avatar.stories.tsx
@@ -4,8 +4,19 @@ import * as Avatar from '@radix-ui/react-avatar';
export default { title: 'Components/Avatar' };
const src = 'https://picsum.photos/id/1005/400/400';
+const otherSrc = 'https://picsum.photos/id/1006/400/400';
const srcBroken = 'https://broken.link.com/broken-pic.jpg';
+const FakeFrameworkImage = (props: any) => {
+ console.log(props);
+
+ return (
+
+
+
+ );
+};
+
export const Styled = () => (
<>
Without image & with fallback
@@ -33,6 +44,36 @@ export const Styled = () => (
+
+ With image framework component
+
+
+
+
+
+
+
+
+
+ With image framework component & with fallback (but broken src)
+
+
+
+
+
+
+
+
>
);
@@ -58,6 +99,36 @@ export const Chromatic = () => (
+
+ With image framework component
+
+
+
+
+
+
+
+
+
+ With image framework component & with fallback (but broken src)
+
+
+
+
+
+
+
+
>
);
Chromatic.parameters = { chromatic: { disable: false, delay: 1000 } };
diff --git a/packages/react/avatar/src/Avatar.test.tsx b/packages/react/avatar/src/Avatar.test.tsx
index 4e71f84ed..1ac65aead 100644
--- a/packages/react/avatar/src/Avatar.test.tsx
+++ b/packages/react/avatar/src/Avatar.test.tsx
@@ -7,6 +7,8 @@ const ROOT_TEST_ID = 'avatar-root';
const FALLBACK_TEXT = 'AB';
const IMAGE_ALT_TEXT = 'Fake Avatar';
const DELAY = 300;
+const FRAMEWORK_IMAGE_TEST_ID = 'framework-image-component';
+const FRAMEWORK_IMAGE_ALT_TEXT = 'framework test';
describe('given an Avatar with fallback and no image', () => {
let rendered: RenderResult;
@@ -33,6 +35,7 @@ describe('given an Avatar with fallback and a working image', () => {
(window.Image as any) = class MockImage {
onload: () => void = () => {};
src: string = '';
+
constructor() {
setTimeout(() => {
this.onload();
@@ -101,6 +104,66 @@ describe('given an Avatar with fallback and delayed render', () => {
});
});
+describe('given an Avatar with fallback and child image', () => {
+ let rendered: RenderResult;
+ let image: HTMLElement | null = null;
+ const orignalGlobalImage = window.Image;
+
+ beforeAll(() => {
+ (window.Image as any) = class MockImage {
+ onload: () => void = () => {};
+ src: string = '';
+
+ constructor() {
+ setTimeout(() => {
+ this.onload();
+ }, DELAY);
+ return this;
+ }
+ };
+ });
+
+ afterAll(() => {
+ window.Image = orignalGlobalImage;
+ });
+
+ beforeEach(() => {
+ rendered = render(
+
+
+
+
+ {FALLBACK_TEXT}
+
+ );
+ console.log(rendered);
+ });
+
+ it('should render the image after it has loaded', async () => {
+ image = await rendered.findByRole('img');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should have alt text on the image', async () => {
+ image = await rendered.findByAltText(FRAMEWORK_IMAGE_ALT_TEXT);
+ expect(image).toBeInTheDocument();
+ });
+
+ it('should render the fallback initially', () => {
+ const fallback = rendered.queryByText(FALLBACK_TEXT);
+ expect(fallback).toBeInTheDocument();
+ });
+
+ it('should render the image framework component', () => {
+ const frameworkImage = rendered.queryByTestId(FRAMEWORK_IMAGE_TEST_ID);
+ expect(frameworkImage).toBeInTheDocument();
+ });
+});
+
describe('given an Avatar with an image that only works when referrerPolicy=no-referrer', () => {
let rendered: RenderResult;
const orignalGlobalImage = window.Image;
diff --git a/packages/react/avatar/src/Avatar.tsx b/packages/react/avatar/src/Avatar.tsx
index 53841b7f0..2fe160388 100644
--- a/packages/react/avatar/src/Avatar.tsx
+++ b/packages/react/avatar/src/Avatar.tsx
@@ -62,8 +62,13 @@ const AvatarImage = React.forwardRef(
(props: ScopedProps, forwardedRef) => {
const { __scopeAvatar, src, onLoadingStatusChange = () => {}, ...imageProps } = props;
const context = useAvatarContext(IMAGE_NAME, __scopeAvatar);
- const imageLoadingStatus = useImageLoadingStatus(src, imageProps.referrerPolicy);
+ const { loadingStatus: imageLoadingStatus, setLoadingStatus } = useImageLoadingStatus(
+ src,
+ props.asChild,
+ imageProps.referrerPolicy
+ );
const handleLoadingStatusChange = useCallbackRef((status: ImageLoadingStatus) => {
+ setLoadingStatus(status);
onLoadingStatusChange(status);
context.onImageLoadingStatusChange(status);
});
@@ -74,6 +79,35 @@ const AvatarImage = React.forwardRef(
}
}, [imageLoadingStatus, handleLoadingStatusChange]);
+ if (props.asChild && props.children) {
+ if (imageLoadingStatus === 'error') {
+ return null;
+ }
+
+ // Ensure children is a valid React element
+ const child = React.Children.only(props.children) as React.ReactElement;
+
+ const { asChild, children, ...restProps } = props;
+
+ const childProps = child.props;
+
+ // Clone the child to add onLoad and onError event listeners
+ return React.cloneElement(child, {
+ ...restProps,
+ ...child.props,
+ onError: (event: React.SyntheticEvent) => {
+ console.log('error');
+ handleLoadingStatusChange('error');
+ if (childProps.onError) childProps.onError(event);
+ },
+ onLoad: (event: React.SyntheticEvent) => {
+ console.log('loaded');
+ handleLoadingStatusChange('loaded');
+ if (childProps.onLoad) childProps.onLoad(event);
+ },
+ });
+ }
+
return imageLoadingStatus === 'loaded' ? (
) : null;
@@ -89,6 +123,7 @@ AvatarImage.displayName = IMAGE_NAME;
const FALLBACK_NAME = 'AvatarFallback';
type AvatarFallbackElement = React.ElementRef;
+
interface AvatarFallbackProps extends PrimitiveSpanProps {
delayMs?: number;
}
@@ -116,10 +151,18 @@ AvatarFallback.displayName = FALLBACK_NAME;
/* -----------------------------------------------------------------------------------------------*/
-function useImageLoadingStatus(src?: string, referrerPolicy?: React.HTMLAttributeReferrerPolicy) {
+function useImageLoadingStatus(
+ src?: string,
+ bypass?: boolean,
+ referrerPolicy?: React.HTMLAttributeReferrerPolicy
+) {
const [loadingStatus, setLoadingStatus] = React.useState('idle');
useLayoutEffect(() => {
+ if (bypass) {
+ setLoadingStatus('idle');
+ return;
+ }
if (!src) {
setLoadingStatus('error');
return;
@@ -144,9 +187,9 @@ function useImageLoadingStatus(src?: string, referrerPolicy?: React.HTMLAttribut
return () => {
isMounted = false;
};
- }, [src, referrerPolicy]);
+ }, [src, bypass, referrerPolicy]);
- return loadingStatus;
+ return { loadingStatus, setLoadingStatus };
}
const Root = Avatar;
const Image = AvatarImage;