Skip to content

Commit 28a5958

Browse files
committed
[avatar] Fix image status edge cases
1 parent bbff4e3 commit 28a5958

6 files changed

Lines changed: 329 additions & 31 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expectType } from '#test-utils';
2+
import {
3+
Avatar,
4+
type AvatarFallbackProps,
5+
type AvatarFallbackState,
6+
type AvatarImageProps,
7+
type AvatarImageState,
8+
type AvatarRootProps,
9+
type AvatarRootState,
10+
type ImageLoadingStatus,
11+
} from '@base-ui/react/avatar';
12+
13+
const rootProps: Avatar.Root.Props = {};
14+
expectType<AvatarRootProps, typeof rootProps>(rootProps);
15+
16+
const imageProps: Avatar.Image.Props = {
17+
crossOrigin: 'anonymous',
18+
referrerPolicy: 'no-referrer',
19+
sizes: '48px',
20+
srcSet: 'avatar.png 1x, avatar@2x.png 2x',
21+
};
22+
expectType<AvatarImageProps, typeof imageProps>(imageProps);
23+
24+
const fallbackProps: Avatar.Fallback.Props = {
25+
delay: 100,
26+
};
27+
expectType<AvatarFallbackProps, typeof fallbackProps>(fallbackProps);
28+
29+
function expectRootState(state: Avatar.Root.State) {
30+
expectType<AvatarRootState, typeof state>(state);
31+
expectType<ImageLoadingStatus, typeof state.imageLoadingStatus>(state.imageLoadingStatus);
32+
}
33+
34+
function expectImageState(state: Avatar.Image.State) {
35+
expectType<AvatarImageState, typeof state>(state);
36+
expectType<ImageLoadingStatus, typeof state.imageLoadingStatus>(state.imageLoadingStatus);
37+
}
38+
39+
function expectFallbackState(state: Avatar.Fallback.State) {
40+
expectType<AvatarFallbackState, typeof state>(state);
41+
expectType<ImageLoadingStatus, typeof state.imageLoadingStatus>(state.imageLoadingStatus);
42+
}
43+
44+
function expectLoadingStatus(status: ImageLoadingStatus) {
45+
expectType<ImageLoadingStatus, typeof status>(status);
46+
}
47+
48+
<Avatar.Root
49+
{...rootProps}
50+
render={(props, state) => {
51+
expectRootState(state);
52+
return <span {...props} />;
53+
}}
54+
>
55+
<Avatar.Image
56+
{...imageProps}
57+
onLoadingStatusChange={(status) => {
58+
expectLoadingStatus(status);
59+
}}
60+
render={(props, state) => {
61+
expectImageState(state);
62+
return <img alt="" {...props} />;
63+
}}
64+
/>
65+
<Avatar.Fallback
66+
{...fallbackProps}
67+
render={(props, state) => {
68+
expectFallbackState(state);
69+
return <span {...props} />;
70+
}}
71+
/>
72+
</Avatar.Root>;

packages/react/src/avatar/fallback/AvatarFallback.test.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Mock, vi, expect } from 'vitest';
22
import * as React from 'react';
33
import { Avatar } from '@base-ui/react/avatar';
4-
import { waitFor, screen } from '@mui/internal-test-utils';
4+
import { act, waitFor, screen } from '@mui/internal-test-utils';
55
import { describeConformance, createRenderer, isJSDOM } from '#test-utils';
66
import { useImageLoadingStatus } from '../image/useImageLoadingStatus';
77

@@ -51,6 +51,38 @@ describe('<Avatar.Fallback />', () => {
5151
});
5252
});
5353

54+
it.skipIf(!isJSDOM)('shows the fallback when a loaded image is unmounted', async () => {
55+
(useImageLoadingStatus as Mock).mockReturnValue('loaded');
56+
57+
function Test() {
58+
const [showImage, setShowImage] = React.useState(true);
59+
60+
return (
61+
<div>
62+
<button onClick={() => setShowImage(false)}>Hide image</button>
63+
<Avatar.Root>
64+
{showImage && <Avatar.Image data-testid="image" src="avatar.png" />}
65+
<Avatar.Fallback data-testid="fallback">AC</Avatar.Fallback>
66+
</Avatar.Root>
67+
</div>
68+
);
69+
}
70+
71+
const { user } = await render(<Test />);
72+
73+
await waitFor(() => {
74+
expect(screen.getByTestId('image')).not.toBe(null);
75+
expect(screen.queryByTestId('fallback')).toBe(null);
76+
});
77+
78+
await user.click(screen.getByText('Hide image'));
79+
80+
await waitFor(() => {
81+
expect(screen.queryByTestId('image')).toBe(null);
82+
expect(screen.getByTestId('fallback')).not.toBe(null);
83+
});
84+
});
85+
5486
describe.skipIf(!isJSDOM)('prop: delay', () => {
5587
const { clock, render: renderFakeTimers } = createRenderer();
5688

@@ -70,6 +102,33 @@ describe('<Avatar.Fallback />', () => {
70102

71103
expect(screen.queryByText('AC')).not.toBe(null);
72104
});
105+
106+
it('shows the fallback when delay changes to undefined', async () => {
107+
(useImageLoadingStatus as Mock).mockReturnValue('error');
108+
let setDelay!: React.Dispatch<React.SetStateAction<number | undefined>>;
109+
110+
function Test() {
111+
const [delay, setDelayState] = React.useState<number | undefined>(100);
112+
setDelay = setDelayState;
113+
114+
return (
115+
<Avatar.Root>
116+
<Avatar.Image />
117+
<Avatar.Fallback delay={delay}>AC</Avatar.Fallback>
118+
</Avatar.Root>
119+
);
120+
}
121+
122+
await renderFakeTimers(<Test />);
123+
124+
expect(screen.queryByText('AC')).toBe(null);
125+
126+
act(() => {
127+
setDelay(undefined);
128+
});
129+
130+
expect(screen.queryByText('AC')).not.toBe(null);
131+
});
73132
});
74133

75134
it.skipIf(!isJSDOM)(

packages/react/src/avatar/fallback/AvatarFallback.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ export const AvatarFallback = React.forwardRef(function AvatarFallback(
2020
const { className, render, delay, style, ...elementProps } = componentProps;
2121

2222
const { imageLoadingStatus } = useAvatarRootContext();
23-
const [delayPassed, setDelayPassed] = React.useState(delay === undefined);
23+
const [delayPassed, setDelayPassed] = React.useState(false);
2424
const timeout = useTimeout();
2525

2626
React.useEffect(() => {
2727
if (delay !== undefined) {
28+
setDelayPassed(false);
2829
timeout.start(delay, () => setDelayPassed(true));
2930
}
3031
return timeout.clear;
@@ -39,7 +40,7 @@ export const AvatarFallback = React.forwardRef(function AvatarFallback(
3940
ref: forwardedRef,
4041
props: elementProps,
4142
stateAttributesMapping: avatarStateAttributesMapping,
42-
enabled: imageLoadingStatus !== 'loaded' && delayPassed,
43+
enabled: imageLoadingStatus !== 'loaded' && (delay === undefined || delayPassed),
4344
});
4445

4546
return element;

0 commit comments

Comments
 (0)