Skip to content

Commit 7b52750

Browse files
committed
Add delay to trigger loading spinner
1 parent 6589db1 commit 7b52750

File tree

1 file changed

+48
-3
lines changed

1 file changed

+48
-3
lines changed

packages/ui/src/elements/Avatar.tsx

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ type AvatarProps = PropsOfComponent<typeof Flex> & {
1919
showLoadingSpinner?: boolean;
2020
};
2121

22+
const SPINNER_DELAY_MS = 150;
23+
const SPINNER_MIN_DURATION_MS = 400;
24+
2225
export const Avatar = (props: AvatarProps) => {
2326
const {
2427
size = () => 26,
@@ -34,14 +37,56 @@ export const Avatar = (props: AvatarProps) => {
3437
} = props;
3538
const [error, setError] = React.useState(false);
3639
const [loaded, setLoaded] = React.useState(false);
40+
const [spinnerVisible, setSpinnerVisible] = React.useState(false);
41+
const spinnerShownAtRef = React.useRef<number | null>(null);
42+
const loadTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
3743

38-
// Reset loaded state when imageUrl changes
3944
React.useEffect(() => {
4045
setLoaded(false);
4146
setError(false);
47+
setSpinnerVisible(false);
48+
spinnerShownAtRef.current = null;
49+
50+
return () => {
51+
if (loadTimerRef.current) {
52+
clearTimeout(loadTimerRef.current);
53+
loadTimerRef.current = null;
54+
}
55+
};
4256
}, [imageUrl]);
4357

44-
const isLoading = showLoadingSpinner && imageUrl && !loaded && !error;
58+
React.useEffect(() => {
59+
if (!showLoadingSpinner || !imageUrl || loaded || error) {
60+
return;
61+
}
62+
63+
const timer = setTimeout(() => {
64+
setSpinnerVisible(true);
65+
spinnerShownAtRef.current = Date.now();
66+
}, SPINNER_DELAY_MS);
67+
68+
return () => clearTimeout(timer);
69+
}, [showLoadingSpinner, imageUrl, loaded, error]);
70+
71+
/**
72+
* Prevents the loading spinner from appearing and disappearing too quickly
73+
*/
74+
const handleImageLoad = React.useCallback(() => {
75+
if (spinnerShownAtRef.current) {
76+
const elapsed = Date.now() - spinnerShownAtRef.current;
77+
const remaining = SPINNER_MIN_DURATION_MS - elapsed;
78+
if (remaining > 0) {
79+
loadTimerRef.current = setTimeout(() => {
80+
loadTimerRef.current = null;
81+
setLoaded(true);
82+
}, remaining);
83+
return;
84+
}
85+
}
86+
setLoaded(true);
87+
}, []);
88+
89+
const isLoading = showLoadingSpinner && spinnerVisible && imageUrl && !loaded && !error;
4590

4691
const ImgOrFallback =
4792
initials && (!imageUrl || error) ? (
@@ -60,7 +105,7 @@ export const Avatar = (props: AvatarProps) => {
60105
transition: 'opacity 0.2s ease-in-out',
61106
}}
62107
onError={() => setError(true)}
63-
onLoad={() => setLoaded(true)}
108+
onLoad={handleImageLoad}
64109
size={imageFetchSize}
65110
/>
66111
);

0 commit comments

Comments
 (0)