@12-apps/ui 6.39.19 → 6.39.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/components/data-display/LazyImage/LazyImage.hooks.js +1 -1
- package/dist/_internal/components/data-display/LazyImage/LazyImage.js +2 -2
- package/dist/_internal/components/data-display/index.js +2 -2
- package/dist/_internal/components/index.js +2 -2
- package/dist/{chunk-GMUYCN33.js → chunk-2TSGRQ2J.js} +34 -14
- package/dist/chunk-2TSGRQ2J.js.map +1 -0
- package/dist/{chunk-BVF2BSWV.js → chunk-QWFAEUFV.js} +28 -10
- package/dist/chunk-QWFAEUFV.js.map +1 -0
- package/dist/data-display/LazyImage.js +2 -2
- package/dist/types/components/data-display/LazyImage/LazyImage.d.ts.map +1 -1
- package/dist/types/components/data-display/LazyImage/LazyImage.hooks.d.ts +1 -1
- package/dist/types/components/data-display/LazyImage/LazyImage.hooks.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/LazyImage/LazyImage.hooks.ts +64 -15
- package/src/components/data-display/LazyImage/LazyImage.md +3 -42
- package/src/components/data-display/LazyImage/LazyImage.tsx +44 -8
- package/dist/chunk-BVF2BSWV.js.map +0 -1
- package/dist/chunk-GMUYCN33.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LazyImage
|
|
3
|
-
} from "../../../../chunk-
|
|
4
|
-
import "../../../../chunk-
|
|
3
|
+
} from "../../../../chunk-QWFAEUFV.js";
|
|
4
|
+
import "../../../../chunk-2TSGRQ2J.js";
|
|
5
5
|
import "../../../../chunk-724ZONOL.js";
|
|
6
6
|
import "../../../../chunk-BEGTLELZ.js";
|
|
7
7
|
import "../../../../chunk-E2QIKJX2.js";
|
|
@@ -91,8 +91,8 @@ import "../../../chunk-S5UY5U73.js";
|
|
|
91
91
|
import "../../../chunk-5ZFO7ROE.js";
|
|
92
92
|
import {
|
|
93
93
|
LazyImage
|
|
94
|
-
} from "../../../chunk-
|
|
95
|
-
import "../../../chunk-
|
|
94
|
+
} from "../../../chunk-QWFAEUFV.js";
|
|
95
|
+
import "../../../chunk-2TSGRQ2J.js";
|
|
96
96
|
import "../../../chunk-724ZONOL.js";
|
|
97
97
|
import "../../../chunk-UM35GGCS.js";
|
|
98
98
|
import {
|
|
@@ -479,8 +479,8 @@ import "../../chunk-S5UY5U73.js";
|
|
|
479
479
|
import "../../chunk-5ZFO7ROE.js";
|
|
480
480
|
import {
|
|
481
481
|
LazyImage
|
|
482
|
-
} from "../../chunk-
|
|
483
|
-
import "../../chunk-
|
|
482
|
+
} from "../../chunk-QWFAEUFV.js";
|
|
483
|
+
import "../../chunk-2TSGRQ2J.js";
|
|
484
484
|
import "../../chunk-724ZONOL.js";
|
|
485
485
|
import "../../chunk-UM35GGCS.js";
|
|
486
486
|
import {
|
|
@@ -21,7 +21,11 @@ var LAZY_IMAGE_DEFAULTS = {
|
|
|
21
21
|
skeletonProps: {},
|
|
22
22
|
spinnerProps: {},
|
|
23
23
|
sx: {},
|
|
24
|
-
role: "img"
|
|
24
|
+
role: "img",
|
|
25
|
+
// FUT-2774 #3: with no default, every derived id (`${testId}-img`, …) read
|
|
26
|
+
// literally as `"undefined-img"` for a caller that omitted it — and every
|
|
27
|
+
// such instance on a page collided on the same ids.
|
|
28
|
+
"data-testid": "lazy-image"
|
|
25
29
|
};
|
|
26
30
|
var definedProps = (props) => Object.fromEntries(
|
|
27
31
|
Object.entries(props).filter(([, value]) => value !== void 0)
|
|
@@ -106,10 +110,14 @@ var useTimeoutRef = () => {
|
|
|
106
110
|
return ref;
|
|
107
111
|
};
|
|
108
112
|
var usePlaceholderPhase = (props, loadingState) => {
|
|
109
|
-
const { placeholder, fadeIn, fadeInDuration } = props;
|
|
113
|
+
const { placeholder, fadeIn, fadeInDuration, src } = props;
|
|
110
114
|
const active = loadingState === "placeholder" && Boolean(placeholder);
|
|
111
115
|
const [retired, setRetired] = useState(false);
|
|
112
116
|
const fadeTimeoutRef = useTimeoutRef();
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
clearTimeout(fadeTimeoutRef.current);
|
|
119
|
+
setRetired(false);
|
|
120
|
+
}, [src, fadeTimeoutRef]);
|
|
113
121
|
const retire = useCallback(() => setRetired(true), []);
|
|
114
122
|
const retireAfterFade = useCallback(() => {
|
|
115
123
|
if (!active || !fadeIn) {
|
|
@@ -135,25 +143,34 @@ var useImageSource = (props, containerRef) => {
|
|
|
135
143
|
retryCount: 0,
|
|
136
144
|
currentSrc: lazy ? null : src
|
|
137
145
|
});
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
const requestedSrcRef = useRef(lazy ? null : src);
|
|
147
|
+
const retryTimeoutRef = useTimeoutRef();
|
|
148
|
+
const markVisible = useCallback(() => {
|
|
149
|
+
requestedSrcRef.current = src;
|
|
150
|
+
setState((prev) => ({ ...prev, isVisible: true, currentSrc: src }));
|
|
151
|
+
}, [src]);
|
|
142
152
|
useVisibility(props, containerRef, state.isVisible, markVisible);
|
|
143
153
|
useEffect(() => {
|
|
144
|
-
if (!state.isVisible || !src ||
|
|
154
|
+
if (!state.isVisible || !src || requestedSrcRef.current === src) {
|
|
145
155
|
return;
|
|
146
156
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
clearTimeout(retryTimeoutRef.current);
|
|
158
|
+
requestedSrcRef.current = src;
|
|
159
|
+
setState((prev) => ({
|
|
160
|
+
...prev,
|
|
161
|
+
currentSrc: src,
|
|
162
|
+
isLoading: true,
|
|
163
|
+
hasError: false,
|
|
164
|
+
retryCount: 0
|
|
165
|
+
}));
|
|
166
|
+
}, [state.isVisible, src, retryTimeoutRef]);
|
|
167
|
+
return { state, setState, requestedSrcRef, retryTimeoutRef };
|
|
150
168
|
};
|
|
151
169
|
var useLazyImage = (props) => {
|
|
152
170
|
const { src, loadingState, showSpinner } = props;
|
|
153
171
|
const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
|
|
154
172
|
const containerRef = useRef(null);
|
|
155
|
-
const retryTimeoutRef =
|
|
156
|
-
const [state, setState] = useImageSource(props, containerRef);
|
|
173
|
+
const { state, setState, requestedSrcRef, retryTimeoutRef } = useImageSource(props, containerRef);
|
|
157
174
|
const effectiveLoadingState = showSpinner ? "spinner" : loadingState;
|
|
158
175
|
const { showPlaceholder, retirePlaceholder, retirePlaceholderAfterFade } = usePlaceholderPhase(
|
|
159
176
|
props,
|
|
@@ -176,6 +193,9 @@ var useLazyImage = (props) => {
|
|
|
176
193
|
return;
|
|
177
194
|
}
|
|
178
195
|
retryTimeoutRef.current = setTimeout(() => {
|
|
196
|
+
if (requestedSrcRef.current !== src) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
179
199
|
setState((prev) => ({
|
|
180
200
|
...prev,
|
|
181
201
|
retryCount: prev.retryCount + 1,
|
|
@@ -184,7 +204,7 @@ var useLazyImage = (props) => {
|
|
|
184
204
|
}));
|
|
185
205
|
}, retryDelay);
|
|
186
206
|
},
|
|
187
|
-
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder]
|
|
207
|
+
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder, requestedSrcRef, retryTimeoutRef]
|
|
188
208
|
);
|
|
189
209
|
return {
|
|
190
210
|
state,
|
|
@@ -206,4 +226,4 @@ export {
|
|
|
206
226
|
imgPassThrough,
|
|
207
227
|
useLazyImage
|
|
208
228
|
};
|
|
209
|
-
//# sourceMappingURL=chunk-
|
|
229
|
+
//# sourceMappingURL=chunk-2TSGRQ2J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/data-display/LazyImage/LazyImage.hooks.ts"],"sourcesContent":["import { useTheme } from '@mui/material/styles/index.js';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { remPx } from '../../../tokens/relative';\n\nimport type { LazyImageProps, LazyImageState } from './LazyImage.types';\n\ntype LazyImageDefaultedKeys =\n | 'height'\n | 'loadingState'\n | 'objectFit'\n | 'objectPosition'\n | 'lazy'\n | 'threshold'\n | 'fadeIn'\n | 'fadeInDuration'\n | 'retryOnError'\n | 'maxRetries'\n | 'retryDelay'\n | 'decoding'\n | 'skeletonProps'\n | 'spinnerProps'\n | 'sx'\n | 'role'\n | 'data-testid';\n\nexport type ResolvedLazyImageProps = LazyImageProps &\n Required<Pick<LazyImageProps, LazyImageDefaultedKeys>>;\n\nconst LAZY_IMAGE_DEFAULTS: Pick<LazyImageProps, LazyImageDefaultedKeys> = {\n height: 'auto',\n loadingState: 'skeleton',\n objectFit: 'cover',\n objectPosition: 'center',\n lazy: true,\n threshold: 0,\n fadeIn: true,\n fadeInDuration: 300,\n retryOnError: false,\n maxRetries: 3,\n retryDelay: 1000,\n decoding: 'async',\n skeletonProps: {},\n spinnerProps: {},\n sx: {},\n role: 'img',\n // FUT-2774 #3: with no default, every derived id (`${testId}-img`, …) read\n // literally as `\"undefined-img\"` for a caller that omitted it — and every\n // such instance on a page collided on the same ids.\n 'data-testid': 'lazy-image',\n};\n\n// Strips explicitly-undefined props before the merge, so `lazy={undefined}` still\n// falls back to the default exactly as a destructuring default would.\nconst definedProps = (props: LazyImageProps): Partial<LazyImageProps> =>\n Object.fromEntries(\n Object.entries(props).filter(([, value]) => value !== undefined),\n ) as Partial<LazyImageProps>;\n\nexport const resolveLazyImageProps = (props: LazyImageProps): ResolvedLazyImageProps =>\n ({ ...LAZY_IMAGE_DEFAULTS, ...definedProps(props) }) as ResolvedLazyImageProps;\n\n// Props this component consumes itself. Anything else the caller passed is an\n// ordinary <img> attribute and is forwarded untouched.\nconst OWN_PROPS = new Set([\n 'src', 'alt', 'width', 'height', 'placeholder', 'fallback', 'loadingState',\n 'showSpinner', 'objectFit', 'objectPosition', 'borderRadius', 'lazy', 'rootMargin',\n 'threshold', 'fadeIn', 'fadeInDuration', 'onLoad', 'onError', 'onLoadStart',\n 'retryOnError', 'maxRetries', 'retryDelay', 'decoding', 'loading', 'fetchPriority',\n 'skeletonProps', 'spinnerProps', 'sx', 'className', 'data-testid', 'aria-label',\n 'aria-describedby', 'role',\n]);\n\nexport const imgPassThrough = (props: ResolvedLazyImageProps): Record<string, unknown> =>\n Object.fromEntries(Object.entries(props).filter(([key]) => !OWN_PROPS.has(key)));\n\n/**\n * Flips `isVisible` the first time the container nears the viewport, then stops\n * observing. Does nothing when `lazy` is off — the caller wants the request now.\n */\nconst useVisibility = (\n props: ResolvedLazyImageProps,\n containerRef: React.RefObject<HTMLDivElement | null>,\n isVisible: boolean,\n onVisible: () => void,\n) => {\n const { lazy, src, threshold, onLoadStart } = props;\n const theme = useTheme();\n // `IntersectionObserver` takes px or %, never rem: the default reach is 100\n // design px, measured through the theme so it moves with the root.\n const rootMargin = props.rootMargin ?? `${remPx(theme, 100)}px`;\n\n useEffect(() => {\n if (!lazy || isVisible) {\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n onVisible();\n onLoadStart?.();\n observer.unobserve(entry.target);\n }\n });\n },\n { rootMargin, threshold },\n );\n\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n\n return () => {\n if (containerRef.current) {\n observer.unobserve(containerRef.current);\n }\n };\n }, [lazy, src, rootMargin, threshold, isVisible, onLoadStart, onVisible, containerRef]);\n};\n\n/** A pending timer has to be cancelled on unmount, or it sets state on a dead component. */\nconst useTimeoutRef = () => {\n const ref = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n useEffect(\n () => () => {\n if (ref.current) {\n clearTimeout(ref.current);\n }\n },\n [],\n );\n\n return ref;\n};\n\n/**\n * Whether the placeholder image is up. It is a loading state: only\n * `loadingState=\"placeholder\"` with a `placeholder` set draws it, from mount\n * until the real image settles — its `load` plus the fade, or its final error.\n * A placeholder that fails to load retires at once, as if none were set.\n */\nconst usePlaceholderPhase = (\n props: ResolvedLazyImageProps,\n loadingState: NonNullable<LazyImageProps['loadingState']>,\n) => {\n const { placeholder, fadeIn, fadeInDuration, src } = props;\n // Only this mode draws a placeholder. Otherwise the real image settling\n // retires the phase at once — batched into the load's own render, with no\n // timer — so a placeholder switched on afterwards never covers a loaded image.\n const active = loadingState === 'placeholder' && Boolean(placeholder);\n const [retired, setRetired] = useState(false);\n const fadeTimeoutRef = useTimeoutRef();\n\n // FUT-2774 #2: `retired` was set once and never reset, and this hook took\n // no `src` at all — so a mounted instance reused for a NEW image (a\n // carousel swapping `src` rather than remounting) never showed the\n // placeholder again for it. Reset on every `src` change, the same way\n // `useImageSource` already resets `currentSrc`/`isLoading`/`hasError`; drop\n // any fade timer left over from the PREVIOUS image, or it would retire the\n // new placeholder on a schedule that has nothing to do with it.\n useEffect(() => {\n clearTimeout(fadeTimeoutRef.current);\n setRetired(false);\n }, [src, fadeTimeoutRef]);\n\n const retire = useCallback(() => setRetired(true), []);\n\n // The real image fades in over the placeholder, so it goes when the fade ends.\n const retireAfterFade = useCallback(() => {\n if (!active || !fadeIn) {\n setRetired(true);\n return;\n }\n clearTimeout(fadeTimeoutRef.current);\n fadeTimeoutRef.current = setTimeout(() => setRetired(true), fadeInDuration);\n }, [active, fadeIn, fadeInDuration, fadeTimeoutRef]);\n\n return {\n showPlaceholder: active && !retired,\n retirePlaceholder: retire,\n retirePlaceholderAfterFade: retireAfterFade,\n };\n};\n\n/**\n * The real image's state, and when it is requested: at once when not lazy,\n * else the first time the container nears the viewport. The main <img> only\n * ever carries the real src — a placeholder is drawn by its own element, so its\n * load and error are never the image's.\n */\nconst useImageSource = (\n props: ResolvedLazyImageProps,\n containerRef: React.RefObject<HTMLDivElement | null>,\n) => {\n const { src, lazy } = props;\n const [state, setState] = useState<LazyImageState>({\n isLoading: true,\n hasError: false,\n isVisible: !lazy, // If not lazy, load immediately\n retryCount: 0,\n currentSrc: lazy ? null : src,\n });\n // FUT-2774 #1: the last `src` PROP this hook has already requested — as\n // opposed to `state.currentSrc`, which a retry deliberately diverges from\n // `src` (its cache-busting query string) without a NEW image having\n // arrived. Comparing the effect's guard against `src` itself let a retry's\n // own state update immediately retrigger this effect, which overwrote the\n // cache-busted value back to the plain `src` before the browser ever\n // re-requested it — so `retryOnError` silently never re-fetched anything.\n const requestedSrcRef = useRef<string | null>(lazy ? null : src);\n // `handleImageError` (in `useLazyImage`) owns the retry timer itself, but\n // it schedules against THIS src's identity: read together, the two hooks\n // can tell a timeout meant for an image that is no longer the current one\n // apart from one that still is (FUT-2774, src-changing-mid-retry).\n const retryTimeoutRef = useTimeoutRef();\n\n const markVisible = useCallback(() => {\n requestedSrcRef.current = src;\n setState((prev) => ({ ...prev, isVisible: true, currentSrc: src }));\n }, [src]);\n useVisibility(props, containerRef, state.isVisible, markVisible);\n\n // Load image when it becomes visible, or when `src` itself changes to a\n // genuinely new value — never when only `state.currentSrc` has (a retry).\n useEffect(() => {\n if (!state.isVisible || !src || requestedSrcRef.current === src) {\n return;\n }\n\n // A retry scheduled for the PREVIOUS src must not land on this one: left\n // pending, its stale `setTimeout` would fire later and overwrite this\n // fresh `currentSrc` with `${oldSrc}?retry=N` — cache-busting an image\n // nothing asked for again, and never re-syncing back (FUT-2774).\n clearTimeout(retryTimeoutRef.current);\n requestedSrcRef.current = src;\n setState((prev) => ({\n ...prev,\n currentSrc: src,\n isLoading: true,\n hasError: false,\n retryCount: 0,\n }));\n }, [state.isVisible, src, retryTimeoutRef]);\n\n return { state, setState, requestedSrcRef, retryTimeoutRef };\n};\n\n/**\n * Owns whether the real image has been requested yet and how it went: the\n * observer above, the load/error handlers, the retry timer, and how long the\n * placeholder stays up.\n */\nexport const useLazyImage = (props: ResolvedLazyImageProps) => {\n const { src, loadingState, showSpinner } = props;\n const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const { state, setState, requestedSrcRef, retryTimeoutRef } = useImageSource(props, containerRef);\n\n // Handle deprecated showSpinner prop\n const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;\n const { showPlaceholder, retirePlaceholder, retirePlaceholderAfterFade } = usePlaceholderPhase(\n props,\n effectiveLoadingState,\n );\n\n const handleImageLoad = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement>) => {\n setState((prev) => ({ ...prev, isLoading: false, hasError: false, retryCount: 0 }));\n retirePlaceholderAfterFade();\n onLoad?.(event);\n },\n [onLoad, retirePlaceholderAfterFade],\n );\n\n const handleImageError = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement>) => {\n if (!retryOnError || state.retryCount >= maxRetries) {\n setState((prev) => ({ ...prev, isLoading: false, hasError: true }));\n retirePlaceholder();\n onError?.(event);\n return;\n }\n\n retryTimeoutRef.current = setTimeout(() => {\n // Belt and suspenders alongside `useImageSource`'s own cancel-on-src-\n // change: if `src` moved on since this timeout was scheduled, this\n // retry is for an image that is no longer live at all — applying it\n // would cache-bust the WRONG `<img>` (FUT-2774).\n if (requestedSrcRef.current !== src) {\n return;\n }\n setState((prev) => ({\n ...prev,\n retryCount: prev.retryCount + 1,\n currentSrc: `${src}?retry=${prev.retryCount + 1}`, // Force reload with cache buster\n }));\n }, retryDelay);\n },\n [onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder, requestedSrcRef, retryTimeoutRef],\n );\n\n return {\n state,\n containerRef,\n effectiveLoadingState,\n handleImageLoad,\n handleImageError,\n handlePlaceholderError: retirePlaceholder,\n showImage: Boolean(state.currentSrc) && !state.hasError,\n // In placeholder mode the placeholder outlasts `isLoading`: it stays under\n // the real image until the fade has ended.\n showLoading: effectiveLoadingState === 'placeholder' ? showPlaceholder : state.isLoading,\n showPlaceholder,\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
imgPassThrough,
|
|
3
3
|
resolveLazyImageProps,
|
|
4
4
|
useLazyImage
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2TSGRQ2J.js";
|
|
6
6
|
import {
|
|
7
7
|
Skeleton
|
|
8
8
|
} from "./chunk-BEGTLELZ.js";
|
|
@@ -60,15 +60,16 @@ var isFraction = (value) => typeof value === "number" && value > 0 && value <= 1
|
|
|
60
60
|
var sxLength = (theme, value) => isFraction(value) ? `${value * 100}%` : styleLength(theme, value);
|
|
61
61
|
var innerLength = (theme, value) => isFraction(value) ? "100%" : styleLength(theme, value);
|
|
62
62
|
var orDefault = (value, fallback) => value === void 0 || value === "" ? fallback : value;
|
|
63
|
+
var emptyToUnset = (value) => value === "" ? void 0 : value;
|
|
63
64
|
var SkeletonIndicator = ({ props }) => {
|
|
64
65
|
const theme = useTheme();
|
|
65
66
|
return /* @__PURE__ */ jsx(
|
|
66
67
|
Skeleton,
|
|
67
68
|
{
|
|
68
|
-
variant: "rectangular",
|
|
69
|
+
variant: props.skeletonProps.variant ?? "rectangular",
|
|
69
70
|
width: innerLength(theme, orDefault(props.width, "100%")),
|
|
70
71
|
height: innerLength(theme, orDefault(props.height, "auto")),
|
|
71
|
-
animation: props.skeletonProps.animation
|
|
72
|
+
animation: props.skeletonProps.animation ?? "pulse",
|
|
72
73
|
intensity: props.skeletonProps.intensity,
|
|
73
74
|
borderRadius: styleLength(theme, props.borderRadius),
|
|
74
75
|
"data-testid": `${props["data-testid"]}-skeleton`
|
|
@@ -81,8 +82,8 @@ var SpinnerIndicator = ({ props }) => {
|
|
|
81
82
|
return /* @__PURE__ */ jsx(SpinnerOverlay, { children: /* @__PURE__ */ jsx(
|
|
82
83
|
CircularProgress,
|
|
83
84
|
{
|
|
84
|
-
size: styleLength(theme, spinnerProps.size
|
|
85
|
-
thickness: spinnerProps.thickness
|
|
85
|
+
size: styleLength(theme, spinnerProps.size ?? 40),
|
|
86
|
+
thickness: spinnerProps.thickness ?? 4,
|
|
86
87
|
sx: { color: spinnerProps.color || theme.palette.primary.main },
|
|
87
88
|
"data-testid": `${props["data-testid"]}-spinner`
|
|
88
89
|
}
|
|
@@ -147,8 +148,19 @@ var ErrorFallback = ({ props, metrics }) => {
|
|
|
147
148
|
FallbackContainer,
|
|
148
149
|
{
|
|
149
150
|
sx: {
|
|
150
|
-
|
|
151
|
-
height
|
|
151
|
+
// FUT-2774 #4 (see `emptyToUnset`). Does NOT change the confirmed
|
|
152
|
+
// height collapse — `ImageContainer`'s own height was already `'auto'`
|
|
153
|
+
// either way; see `LazyImage.test.stories.tsx` for that separate,
|
|
154
|
+
// unresolved defect. `orDefault(height, 'auto')` is safe to share with
|
|
155
|
+
// `SkeletonIndicator` (unlike width's `'100%'`, deliberately NOT
|
|
156
|
+
// shared — see `emptyToUnset`): a PERCENTAGE height against an
|
|
157
|
+
// auto-height containing block computes to `auto` by spec regardless,
|
|
158
|
+
// so whether `sx.height` is omitted (falling through to
|
|
159
|
+
// `FallbackContainer`'s own `height: '100%'`) or set to the literal
|
|
160
|
+
// `'auto'` here, an unset-height container gives the same result
|
|
161
|
+
// either way — nothing for this fallback to protect against.
|
|
162
|
+
width: innerLength(theme, emptyToUnset(width)),
|
|
163
|
+
height: innerLength(theme, orDefault(height, "auto")),
|
|
152
164
|
borderRadius: styleLength(theme, borderRadius)
|
|
153
165
|
},
|
|
154
166
|
"data-testid": `${testId}-fallback`,
|
|
@@ -173,8 +185,14 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
173
185
|
const { width, height, borderRadius, alt } = props;
|
|
174
186
|
const testId = props["data-testid"];
|
|
175
187
|
const metrics = {
|
|
176
|
-
|
|
177
|
-
|
|
188
|
+
// FUT-2774 #4 (see `emptyToUnset`): `metrics` also sizes the REAL `<img>`,
|
|
189
|
+
// where an unset width means "the image's own intrinsic size" —
|
|
190
|
+
// `brand-link.tsx`'s logo relies on exactly that. `height`'s `'auto'`
|
|
191
|
+
// fallback has no such live case to protect: a percentage height against
|
|
192
|
+
// an auto-height containing block computes to `auto` regardless, so
|
|
193
|
+
// sharing `SkeletonIndicator`'s fallback changes nothing observable.
|
|
194
|
+
width: innerLength(theme, emptyToUnset(width)),
|
|
195
|
+
height: innerLength(theme, orDefault(height, "auto")),
|
|
178
196
|
objectFit: props.objectFit,
|
|
179
197
|
objectPosition: props.objectPosition,
|
|
180
198
|
borderRadius: styleLength(theme, borderRadius)
|
|
@@ -232,4 +250,4 @@ LazyImage.displayName = "LazyImage";
|
|
|
232
250
|
export {
|
|
233
251
|
LazyImage
|
|
234
252
|
};
|
|
235
|
-
//# sourceMappingURL=chunk-
|
|
253
|
+
//# sourceMappingURL=chunk-QWFAEUFV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/data-display/LazyImage/LazyImage.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport CircularProgress from '@mui/material/CircularProgress/index.js';\nimport { styled, useTheme, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\nimport { Skeleton } from '../../layout/Skeleton';\nimport type { ResolvedLazyImageProps } from './LazyImage.hooks';\nimport { imgPassThrough, resolveLazyImageProps, useLazyImage } from './LazyImage.hooks';\nimport type { LazyImageProps } from './LazyImage.types';\nimport { sheen } from '../../../tokens/ink';\nimport { rem } from '../../../tokens/relative';\n\nconst ImageContainer = styled(Box)(() => ({\n position: 'relative',\n display: 'inline-block',\n overflow: 'hidden',\n}));\n\nconst StyledImage = styled('img')<{\n fadeIn?: boolean;\n fadeInDuration?: number;\n isLoaded?: boolean;\n}>(({ fadeIn, fadeInDuration = 300, isLoaded }) => ({\n display: 'block',\n maxWidth: '100%',\n height: 'auto',\n opacity: fadeIn ? (isLoaded ? 1 : 0) : 1,\n transition: fadeIn ? `opacity ${fadeInDuration}ms ease-in-out` : 'none',\n}));\n\nconst SpinnerOverlay = styled(Box)(({ theme }) => ({\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: sheen(theme, 0.8),\n borderRadius: '50%',\n padding: theme.spacing(1),\n}));\n\nconst FallbackContainer = styled(Box)(({ theme }) => ({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: theme.palette.action.disabledBackground,\n color: theme.palette.text.secondary,\n width: '100%',\n height: '100%',\n position: 'absolute',\n top: 0,\n left: 0,\n}));\n\ntype Length = number | string | undefined;\n\n/** A length prop as React `style` and MUI's `Skeleton` read it: a number is design px. */\nconst styleLength = (theme: Theme, value: Length): string | undefined =>\n typeof value === 'number' ? rem(theme, value) : value;\n\n/**\n * A width or height above 0 and up to 1 is a fraction of the container's parent, as in `sx`.\n * A negative number is no fraction: it stays a (negative, so invalid) length.\n */\nconst isFraction = (value: Length): value is number =>\n typeof value === 'number' && value > 0 && value <= 1;\n\n/**\n * A length prop as `sx` reads it on a width or height: a number of 1 or less is\n * a fraction of the parent, and that stays so; any other number is design px.\n */\nconst sxLength = (theme: Theme, value: Length): string | undefined =>\n isFraction(value) ? `${value * 100}%` : styleLength(theme, value);\n\n/**\n * A width or height for what is drawn inside the container. The container alone\n * takes a fraction, so everything in it fills it: a percentage here is measured\n * against the container and would stack (FUT-2666). Otherwise the same as `styleLength`.\n */\nconst innerLength = (theme: Theme, value: Length): string | undefined =>\n isFraction(value) ? '100%' : styleLength(theme, value);\n\n/**\n * A width or height with its default. Only an unset or empty size takes it: `0`\n * is a size, which `||` read as unset (FUT-2669), and `sx` reads `''` as `0%`.\n */\nconst orDefault = (value: Length, fallback: number | string): number | string =>\n value === undefined || value === '' ? fallback : value;\n\n/**\n * A width/height as `metrics` and the `ReactNode` `ErrorFallback` read it\n * (FUT-2774 #4): an explicit empty string is normalized to \"as if unset\"\n * rather than mapped to a component-wide default. Unlike `SkeletonIndicator`\n * (a content-less box with nothing else to size by, hence its own deliberate\n * `'100%'`/`'auto'` fallback), these draw the real image or a `ReactNode`\n * fallback — both have their own intrinsic size, so leaving an unset width\n * unset lets it size naturally (`brand-link.tsx`'s logo relies on exactly\n * this: an unset width capped by its own `sx.maxWidth`).\n */\nconst emptyToUnset = (value: Length): Length => (value === '' ? undefined : value);\n\n/** The box the image occupies, as CSS, shared by the real image and every stand-in for it. */\ninterface BoxMetrics {\n width?: string;\n height?: string;\n objectFit: NonNullable<LazyImageProps['objectFit']>;\n objectPosition: string;\n borderRadius?: string;\n}\n\ninterface IndicatorProps {\n props: ResolvedLazyImageProps;\n metrics: BoxMetrics;\n}\n\nconst SkeletonIndicator: React.FC<IndicatorProps> = ({ props }) => {\n const theme = useTheme();\n return (\n <Skeleton\n // FUT-2774 #5: `skeletonProps.variant` was typed but never read — the\n // variant was hard-coded regardless of what a caller passed.\n variant={props.skeletonProps.variant ?? 'rectangular'}\n width={innerLength(theme, orDefault(props.width, '100%'))}\n height={innerLength(theme, orDefault(props.height, 'auto'))}\n // FUT-2774 #5: `||` reads a caller's `false` the same as \"unset\", so\n // `skeletonProps.animation={false}` could never turn the animation off.\n animation={props.skeletonProps.animation ?? 'pulse'}\n intensity={props.skeletonProps.intensity}\n borderRadius={styleLength(theme, props.borderRadius)}\n data-testid={`${props['data-testid']}-skeleton`}\n />\n );\n};\n\nconst SpinnerIndicator: React.FC<IndicatorProps> = ({ props }) => {\n const theme = useTheme();\n const { spinnerProps } = props;\n\n return (\n <SpinnerOverlay>\n <CircularProgress\n // FUT-2774 #5: `||` read `spinnerProps.size`/`thickness` of `0` the\n // same as unset — the same class of bug FUT-2669 fixed for\n // `width`/`height`, missed here.\n size={styleLength(theme, spinnerProps.size ?? 40)}\n thickness={spinnerProps.thickness ?? 4}\n sx={{ color: spinnerProps.color || theme.palette.primary.main }}\n data-testid={`${props['data-testid']}-spinner`}\n />\n </SpinnerOverlay>\n );\n};\n\n/**\n * The placeholder sits in the box's flow and gives the box its size: the box's\n * own when one is set, the placeholder's when not. The real image is drawn over\n * it (`OVER_PLACEHOLDER`) until it has faded in. Its load and error are not the\n * image's, so neither reaches the caller; a failing one just retires.\n */\nconst PlaceholderIndicator: React.FC<IndicatorProps & { onError: () => void }> = ({\n props,\n metrics,\n onError,\n}) => {\n if (!props.placeholder) return null;\n\n return (\n <StyledImage\n src={props.placeholder}\n alt={`${props.alt} (loading)`}\n style={metrics}\n decoding={props.decoding}\n loading={props.lazy ? 'lazy' : props.loading}\n onError={onError}\n data-testid={`${props['data-testid']}-placeholder`}\n />\n );\n};\n\nconst LoadingIndicator: React.FC<\n IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>; onPlaceholderError: () => void }\n> = ({ kind, onPlaceholderError, ...rest }) => {\n switch (kind) {\n case 'skeleton':\n return <SkeletonIndicator {...rest} />;\n case 'spinner':\n return <SpinnerIndicator {...rest} />;\n case 'placeholder':\n return <PlaceholderIndicator {...rest} onError={onPlaceholderError} />;\n default:\n return null;\n }\n};\n\n/**\n * The real image while the placeholder is up: laid over it and cropped by\n * `objectFit`, so the box never grows to hold both. It takes the flow back\n * when the placeholder goes.\n */\nconst OVER_PLACEHOLDER: React.CSSProperties = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n};\n\nconst ErrorFallback: React.FC<IndicatorProps> = ({ props, metrics }) => {\n const theme = useTheme();\n const { alt, fallback, width, height, borderRadius } = props;\n const testId = props['data-testid'];\n\n if (!fallback) return null;\n\n if (typeof fallback === 'string') {\n return (\n <StyledImage\n src={fallback}\n alt={`${alt} (fallback)`}\n style={metrics}\n fadeIn={false}\n isLoaded={true}\n data-testid={`${testId}-fallback`}\n />\n );\n }\n\n return (\n <FallbackContainer\n sx={{\n // FUT-2774 #4 (see `emptyToUnset`). Does NOT change the confirmed\n // height collapse — `ImageContainer`'s own height was already `'auto'`\n // either way; see `LazyImage.test.stories.tsx` for that separate,\n // unresolved defect. `orDefault(height, 'auto')` is safe to share with\n // `SkeletonIndicator` (unlike width's `'100%'`, deliberately NOT\n // shared — see `emptyToUnset`): a PERCENTAGE height against an\n // auto-height containing block computes to `auto` by spec regardless,\n // so whether `sx.height` is omitted (falling through to\n // `FallbackContainer`'s own `height: '100%'`) or set to the literal\n // `'auto'` here, an unset-height container gives the same result\n // either way — nothing for this fallback to protect against.\n width: innerLength(theme, emptyToUnset(width)),\n height: innerLength(theme, orDefault(height, 'auto')),\n borderRadius: styleLength(theme, borderRadius),\n }}\n data-testid={`${testId}-fallback`}\n >\n {fallback}\n </FallbackContainer>\n );\n};\n\n/**\n * LazyImage component with enhanced features for optimized image loading\n * Supports lazy loading, placeholders, error handling, and various loading states\n */\nexport const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps) {\n const props = resolveLazyImageProps(rawProps);\n const theme = useTheme();\n const {\n state,\n containerRef,\n effectiveLoadingState,\n handleImageLoad,\n handleImageError,\n handlePlaceholderError,\n showImage,\n showLoading,\n showPlaceholder,\n } = useLazyImage(props);\n\n const { width, height, borderRadius, alt } = props;\n const testId = props['data-testid'];\n const metrics: BoxMetrics = {\n // FUT-2774 #4 (see `emptyToUnset`): `metrics` also sizes the REAL `<img>`,\n // where an unset width means \"the image's own intrinsic size\" —\n // `brand-link.tsx`'s logo relies on exactly that. `height`'s `'auto'`\n // fallback has no such live case to protect: a percentage height against\n // an auto-height containing block computes to `auto` regardless, so\n // sharing `SkeletonIndicator`'s fallback changes nothing observable.\n width: innerLength(theme, emptyToUnset(width)),\n height: innerLength(theme, orDefault(height, 'auto')),\n objectFit: props.objectFit,\n objectPosition: props.objectPosition,\n borderRadius: styleLength(theme, borderRadius),\n };\n\n return (\n <ImageContainer\n ref={containerRef}\n className={props.className}\n sx={{\n width: sxLength(theme, orDefault(width, 'auto')),\n height: sxLength(theme, orDefault(height, 'auto')),\n // The same px length the image gets (`metrics`): in `sx` a bare number\n // would be a multiple of `shape.borderRadius`, and this box clips (FUT-2656).\n borderRadius: metrics.borderRadius,\n }}\n data-testid={testId}\n >\n {showLoading && (\n <LoadingIndicator\n kind={effectiveLoadingState}\n props={props}\n metrics={metrics}\n onPlaceholderError={handlePlaceholderError}\n />\n )}\n\n {showImage && (\n <StyledImage\n src={state.currentSrc || undefined}\n alt={alt}\n onLoad={handleImageLoad}\n onError={handleImageError}\n fadeIn={props.fadeIn}\n fadeInDuration={props.fadeInDuration}\n isLoaded={!state.isLoading}\n decoding={props.decoding}\n loading={props.lazy ? 'lazy' : props.loading}\n style={{ ...metrics, ...props.sx, ...(showPlaceholder && OVER_PLACEHOLDER) }}\n aria-label={props['aria-label'] || alt}\n aria-describedby={props['aria-describedby']}\n role={props.role}\n data-testid={`${testId}-img`}\n {...imgPassThrough(props)}\n />\n )}\n\n {state.hasError && <ErrorFallback props={props} metrics={metrics} />}\n </ImageContainer>\n );\n});\n\nLazyImage.displayName = 'LazyImage';\n"],"mappings":"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","names":["LazyImage"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "../chunk-5ZFO7ROE.js";
|
|
2
2
|
import {
|
|
3
3
|
LazyImage
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-QWFAEUFV.js";
|
|
5
|
+
import "../chunk-2TSGRQ2J.js";
|
|
6
6
|
import "../chunk-724ZONOL.js";
|
|
7
7
|
import "../chunk-BEGTLELZ.js";
|
|
8
8
|
import "../chunk-E2QIKJX2.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LazyImage.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"LazyImage.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAqPxD;;;GAGG;AACH,eAAO,MAAM,SAAS,4CA4EpB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { LazyImageProps, LazyImageState } from './LazyImage.types';
|
|
3
|
-
type LazyImageDefaultedKeys = 'height' | 'loadingState' | 'objectFit' | 'objectPosition' | 'lazy' | 'threshold' | 'fadeIn' | 'fadeInDuration' | 'retryOnError' | 'maxRetries' | 'retryDelay' | 'decoding' | 'skeletonProps' | 'spinnerProps' | 'sx' | 'role';
|
|
3
|
+
type LazyImageDefaultedKeys = 'height' | 'loadingState' | 'objectFit' | 'objectPosition' | 'lazy' | 'threshold' | 'fadeIn' | 'fadeInDuration' | 'retryOnError' | 'maxRetries' | 'retryDelay' | 'decoding' | 'skeletonProps' | 'spinnerProps' | 'sx' | 'role' | 'data-testid';
|
|
4
4
|
export type ResolvedLazyImageProps = LazyImageProps & Required<Pick<LazyImageProps, LazyImageDefaultedKeys>>;
|
|
5
5
|
export declare const resolveLazyImageProps: (props: LazyImageProps) => ResolvedLazyImageProps;
|
|
6
6
|
export declare const imgPassThrough: (props: ResolvedLazyImageProps) => Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LazyImage.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExE,KAAK,sBAAsB,GACvB,QAAQ,GACR,cAAc,GACd,WAAW,GACX,gBAAgB,GAChB,MAAM,GACN,WAAW,GACX,QAAQ,GACR,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,eAAe,GACf,cAAc,GACd,IAAI,GACJ,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"LazyImage.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExE,KAAK,sBAAsB,GACvB,QAAQ,GACR,cAAc,GACd,WAAW,GACX,gBAAgB,GAChB,MAAM,GACN,WAAW,GACX,QAAQ,GACR,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,eAAe,GACf,cAAc,GACd,IAAI,GACJ,MAAM,GACN,aAAa,CAAC;AAElB,MAAM,MAAM,sBAAsB,GAAG,cAAc,GACjD,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,sBAAsB,CAAC,CAAC,CAAC;AAgCzD,eAAO,MAAM,qBAAqB,GAAI,OAAO,cAAc,KAAG,sBACkB,CAAC;AAajF,eAAO,MAAM,cAAc,GAAI,OAAO,sBAAsB,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,CAAC;AAgLnF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,sBAAsB;;;;6BAe9C,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;8BAStC,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;;;;;CAuCjD,CAAC"}
|
package/package.json
CHANGED
|
@@ -21,7 +21,8 @@ type LazyImageDefaultedKeys =
|
|
|
21
21
|
| 'skeletonProps'
|
|
22
22
|
| 'spinnerProps'
|
|
23
23
|
| 'sx'
|
|
24
|
-
| 'role'
|
|
24
|
+
| 'role'
|
|
25
|
+
| 'data-testid';
|
|
25
26
|
|
|
26
27
|
export type ResolvedLazyImageProps = LazyImageProps &
|
|
27
28
|
Required<Pick<LazyImageProps, LazyImageDefaultedKeys>>;
|
|
@@ -43,6 +44,10 @@ const LAZY_IMAGE_DEFAULTS: Pick<LazyImageProps, LazyImageDefaultedKeys> = {
|
|
|
43
44
|
spinnerProps: {},
|
|
44
45
|
sx: {},
|
|
45
46
|
role: 'img',
|
|
47
|
+
// FUT-2774 #3: with no default, every derived id (`${testId}-img`, …) read
|
|
48
|
+
// literally as `"undefined-img"` for a caller that omitted it — and every
|
|
49
|
+
// such instance on a page collided on the same ids.
|
|
50
|
+
'data-testid': 'lazy-image',
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
// Strips explicitly-undefined props before the merge, so `lazy={undefined}` still
|
|
@@ -141,7 +146,7 @@ const usePlaceholderPhase = (
|
|
|
141
146
|
props: ResolvedLazyImageProps,
|
|
142
147
|
loadingState: NonNullable<LazyImageProps['loadingState']>,
|
|
143
148
|
) => {
|
|
144
|
-
const { placeholder, fadeIn, fadeInDuration } = props;
|
|
149
|
+
const { placeholder, fadeIn, fadeInDuration, src } = props;
|
|
145
150
|
// Only this mode draws a placeholder. Otherwise the real image settling
|
|
146
151
|
// retires the phase at once — batched into the load's own render, with no
|
|
147
152
|
// timer — so a placeholder switched on afterwards never covers a loaded image.
|
|
@@ -149,6 +154,18 @@ const usePlaceholderPhase = (
|
|
|
149
154
|
const [retired, setRetired] = useState(false);
|
|
150
155
|
const fadeTimeoutRef = useTimeoutRef();
|
|
151
156
|
|
|
157
|
+
// FUT-2774 #2: `retired` was set once and never reset, and this hook took
|
|
158
|
+
// no `src` at all — so a mounted instance reused for a NEW image (a
|
|
159
|
+
// carousel swapping `src` rather than remounting) never showed the
|
|
160
|
+
// placeholder again for it. Reset on every `src` change, the same way
|
|
161
|
+
// `useImageSource` already resets `currentSrc`/`isLoading`/`hasError`; drop
|
|
162
|
+
// any fade timer left over from the PREVIOUS image, or it would retire the
|
|
163
|
+
// new placeholder on a schedule that has nothing to do with it.
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
clearTimeout(fadeTimeoutRef.current);
|
|
166
|
+
setRetired(false);
|
|
167
|
+
}, [src, fadeTimeoutRef]);
|
|
168
|
+
|
|
152
169
|
const retire = useCallback(() => setRetired(true), []);
|
|
153
170
|
|
|
154
171
|
// The real image fades in over the placeholder, so it goes when the fade ends.
|
|
@@ -186,23 +203,49 @@ const useImageSource = (
|
|
|
186
203
|
retryCount: 0,
|
|
187
204
|
currentSrc: lazy ? null : src,
|
|
188
205
|
});
|
|
206
|
+
// FUT-2774 #1: the last `src` PROP this hook has already requested — as
|
|
207
|
+
// opposed to `state.currentSrc`, which a retry deliberately diverges from
|
|
208
|
+
// `src` (its cache-busting query string) without a NEW image having
|
|
209
|
+
// arrived. Comparing the effect's guard against `src` itself let a retry's
|
|
210
|
+
// own state update immediately retrigger this effect, which overwrote the
|
|
211
|
+
// cache-busted value back to the plain `src` before the browser ever
|
|
212
|
+
// re-requested it — so `retryOnError` silently never re-fetched anything.
|
|
213
|
+
const requestedSrcRef = useRef<string | null>(lazy ? null : src);
|
|
214
|
+
// `handleImageError` (in `useLazyImage`) owns the retry timer itself, but
|
|
215
|
+
// it schedules against THIS src's identity: read together, the two hooks
|
|
216
|
+
// can tell a timeout meant for an image that is no longer the current one
|
|
217
|
+
// apart from one that still is (FUT-2774, src-changing-mid-retry).
|
|
218
|
+
const retryTimeoutRef = useTimeoutRef();
|
|
189
219
|
|
|
190
|
-
const markVisible = useCallback(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
220
|
+
const markVisible = useCallback(() => {
|
|
221
|
+
requestedSrcRef.current = src;
|
|
222
|
+
setState((prev) => ({ ...prev, isVisible: true, currentSrc: src }));
|
|
223
|
+
}, [src]);
|
|
194
224
|
useVisibility(props, containerRef, state.isVisible, markVisible);
|
|
195
225
|
|
|
196
|
-
// Load image when it becomes visible
|
|
226
|
+
// Load image when it becomes visible, or when `src` itself changes to a
|
|
227
|
+
// genuinely new value — never when only `state.currentSrc` has (a retry).
|
|
197
228
|
useEffect(() => {
|
|
198
|
-
if (!state.isVisible || !src ||
|
|
229
|
+
if (!state.isVisible || !src || requestedSrcRef.current === src) {
|
|
199
230
|
return;
|
|
200
231
|
}
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
233
|
+
// A retry scheduled for the PREVIOUS src must not land on this one: left
|
|
234
|
+
// pending, its stale `setTimeout` would fire later and overwrite this
|
|
235
|
+
// fresh `currentSrc` with `${oldSrc}?retry=N` — cache-busting an image
|
|
236
|
+
// nothing asked for again, and never re-syncing back (FUT-2774).
|
|
237
|
+
clearTimeout(retryTimeoutRef.current);
|
|
238
|
+
requestedSrcRef.current = src;
|
|
239
|
+
setState((prev) => ({
|
|
240
|
+
...prev,
|
|
241
|
+
currentSrc: src,
|
|
242
|
+
isLoading: true,
|
|
243
|
+
hasError: false,
|
|
244
|
+
retryCount: 0,
|
|
245
|
+
}));
|
|
246
|
+
}, [state.isVisible, src, retryTimeoutRef]);
|
|
247
|
+
|
|
248
|
+
return { state, setState, requestedSrcRef, retryTimeoutRef };
|
|
206
249
|
};
|
|
207
250
|
|
|
208
251
|
/**
|
|
@@ -215,8 +258,7 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
215
258
|
const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
|
|
216
259
|
|
|
217
260
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
218
|
-
const retryTimeoutRef =
|
|
219
|
-
const [state, setState] = useImageSource(props, containerRef);
|
|
261
|
+
const { state, setState, requestedSrcRef, retryTimeoutRef } = useImageSource(props, containerRef);
|
|
220
262
|
|
|
221
263
|
// Handle deprecated showSpinner prop
|
|
222
264
|
const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;
|
|
@@ -244,6 +286,13 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
244
286
|
}
|
|
245
287
|
|
|
246
288
|
retryTimeoutRef.current = setTimeout(() => {
|
|
289
|
+
// Belt and suspenders alongside `useImageSource`'s own cancel-on-src-
|
|
290
|
+
// change: if `src` moved on since this timeout was scheduled, this
|
|
291
|
+
// retry is for an image that is no longer live at all — applying it
|
|
292
|
+
// would cache-bust the WRONG `<img>` (FUT-2774).
|
|
293
|
+
if (requestedSrcRef.current !== src) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
247
296
|
setState((prev) => ({
|
|
248
297
|
...prev,
|
|
249
298
|
retryCount: prev.retryCount + 1,
|
|
@@ -251,7 +300,7 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
251
300
|
}));
|
|
252
301
|
}, retryDelay);
|
|
253
302
|
},
|
|
254
|
-
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder],
|
|
303
|
+
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder, requestedSrcRef, retryTimeoutRef],
|
|
255
304
|
);
|
|
256
305
|
|
|
257
306
|
return {
|
|
@@ -16,7 +16,7 @@ The LazyImage component optimizes image loading performance by:
|
|
|
16
16
|
### Basic Usage
|
|
17
17
|
|
|
18
18
|
```tsx
|
|
19
|
-
import { LazyImage } from '@
|
|
19
|
+
import { LazyImage } from '@12-apps/ui';
|
|
20
20
|
|
|
21
21
|
function App() {
|
|
22
22
|
return (
|
|
@@ -225,7 +225,7 @@ wrapping element, rounded to match the image:
|
|
|
225
225
|
| Prop | Type | Default | Description |
|
|
226
226
|
|------|------|---------|-------------|
|
|
227
227
|
| `decoding` | `'async' \| 'sync' \| 'auto'` | `'async'` | Image decoding hint |
|
|
228
|
-
| `loading` | `'eager' \| 'lazy'
|
|
228
|
+
| `loading` | `'eager' \| 'lazy'` | - | Native `loading` attribute; independent of `lazy` (which gates the `IntersectionObserver`) |
|
|
229
229
|
| `fetchPriority` | `'high' \| 'low' \| 'auto'` | - | Fetch priority hint |
|
|
230
230
|
|
|
231
231
|
### Customization Props
|
|
@@ -334,48 +334,9 @@ The LazyImage component follows accessibility best practices:
|
|
|
334
334
|
1. **Lazy Loading**: Images load only when needed, reducing initial page load
|
|
335
335
|
2. **Intersection Observer**: Efficient viewport detection with configurable margins
|
|
336
336
|
3. **Progressive Loading**: Support for placeholder images during load
|
|
337
|
-
4. **Retry Logic**: Automatic retry for failed loads
|
|
337
|
+
4. **Retry Logic**: Automatic retry for failed loads, at a fixed `retryDelay` — the delay does NOT grow between attempts
|
|
338
338
|
5. **Native Loading**: Leverages browser's native lazy loading when available
|
|
339
339
|
|
|
340
|
-
## Migration from Original LazyImage
|
|
341
|
-
|
|
342
|
-
If migrating from the original LazyImage in status-site:
|
|
343
|
-
|
|
344
|
-
```tsx
|
|
345
|
-
// Old usage
|
|
346
|
-
import { LazyImage } from '../components/LazyImage';
|
|
347
|
-
|
|
348
|
-
<LazyImage
|
|
349
|
-
src="image.jpg"
|
|
350
|
-
alt="Description"
|
|
351
|
-
width={400}
|
|
352
|
-
height={300}
|
|
353
|
-
sx={{ borderRadius: 4 }}
|
|
354
|
-
/>
|
|
355
|
-
|
|
356
|
-
// New usage (fully backward compatible)
|
|
357
|
-
import { LazyImage } from '@app-services-monitoring/ui';
|
|
358
|
-
|
|
359
|
-
<LazyImage
|
|
360
|
-
src="image.jpg"
|
|
361
|
-
alt="Description"
|
|
362
|
-
width={400}
|
|
363
|
-
height={300}
|
|
364
|
-
sx={{ borderRadius: 4 }}
|
|
365
|
-
/>
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### New Features Available After Migration
|
|
369
|
-
|
|
370
|
-
- Error handling with fallback
|
|
371
|
-
- Multiple loading states
|
|
372
|
-
- Retry mechanism
|
|
373
|
-
- Placeholder support
|
|
374
|
-
- Spinner overlay option
|
|
375
|
-
- Event callbacks
|
|
376
|
-
- Better TypeScript support
|
|
377
|
-
- Performance optimizations
|
|
378
|
-
|
|
379
340
|
## Related Components
|
|
380
341
|
|
|
381
342
|
- [Avatar](../Avatar/Avatar.md) - For user profile images
|
|
@@ -88,6 +88,18 @@ const innerLength = (theme: Theme, value: Length): string | undefined =>
|
|
|
88
88
|
const orDefault = (value: Length, fallback: number | string): number | string =>
|
|
89
89
|
value === undefined || value === '' ? fallback : value;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A width/height as `metrics` and the `ReactNode` `ErrorFallback` read it
|
|
93
|
+
* (FUT-2774 #4): an explicit empty string is normalized to "as if unset"
|
|
94
|
+
* rather than mapped to a component-wide default. Unlike `SkeletonIndicator`
|
|
95
|
+
* (a content-less box with nothing else to size by, hence its own deliberate
|
|
96
|
+
* `'100%'`/`'auto'` fallback), these draw the real image or a `ReactNode`
|
|
97
|
+
* fallback — both have their own intrinsic size, so leaving an unset width
|
|
98
|
+
* unset lets it size naturally (`brand-link.tsx`'s logo relies on exactly
|
|
99
|
+
* this: an unset width capped by its own `sx.maxWidth`).
|
|
100
|
+
*/
|
|
101
|
+
const emptyToUnset = (value: Length): Length => (value === '' ? undefined : value);
|
|
102
|
+
|
|
91
103
|
/** The box the image occupies, as CSS, shared by the real image and every stand-in for it. */
|
|
92
104
|
interface BoxMetrics {
|
|
93
105
|
width?: string;
|
|
@@ -106,10 +118,14 @@ const SkeletonIndicator: React.FC<IndicatorProps> = ({ props }) => {
|
|
|
106
118
|
const theme = useTheme();
|
|
107
119
|
return (
|
|
108
120
|
<Skeleton
|
|
109
|
-
variant
|
|
121
|
+
// FUT-2774 #5: `skeletonProps.variant` was typed but never read — the
|
|
122
|
+
// variant was hard-coded regardless of what a caller passed.
|
|
123
|
+
variant={props.skeletonProps.variant ?? 'rectangular'}
|
|
110
124
|
width={innerLength(theme, orDefault(props.width, '100%'))}
|
|
111
125
|
height={innerLength(theme, orDefault(props.height, 'auto'))}
|
|
112
|
-
|
|
126
|
+
// FUT-2774 #5: `||` reads a caller's `false` the same as "unset", so
|
|
127
|
+
// `skeletonProps.animation={false}` could never turn the animation off.
|
|
128
|
+
animation={props.skeletonProps.animation ?? 'pulse'}
|
|
113
129
|
intensity={props.skeletonProps.intensity}
|
|
114
130
|
borderRadius={styleLength(theme, props.borderRadius)}
|
|
115
131
|
data-testid={`${props['data-testid']}-skeleton`}
|
|
@@ -124,8 +140,11 @@ const SpinnerIndicator: React.FC<IndicatorProps> = ({ props }) => {
|
|
|
124
140
|
return (
|
|
125
141
|
<SpinnerOverlay>
|
|
126
142
|
<CircularProgress
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
// FUT-2774 #5: `||` read `spinnerProps.size`/`thickness` of `0` the
|
|
144
|
+
// same as unset — the same class of bug FUT-2669 fixed for
|
|
145
|
+
// `width`/`height`, missed here.
|
|
146
|
+
size={styleLength(theme, spinnerProps.size ?? 40)}
|
|
147
|
+
thickness={spinnerProps.thickness ?? 4}
|
|
129
148
|
sx={{ color: spinnerProps.color || theme.palette.primary.main }}
|
|
130
149
|
data-testid={`${props['data-testid']}-spinner`}
|
|
131
150
|
/>
|
|
@@ -209,8 +228,19 @@ const ErrorFallback: React.FC<IndicatorProps> = ({ props, metrics }) => {
|
|
|
209
228
|
return (
|
|
210
229
|
<FallbackContainer
|
|
211
230
|
sx={{
|
|
212
|
-
|
|
213
|
-
height
|
|
231
|
+
// FUT-2774 #4 (see `emptyToUnset`). Does NOT change the confirmed
|
|
232
|
+
// height collapse — `ImageContainer`'s own height was already `'auto'`
|
|
233
|
+
// either way; see `LazyImage.test.stories.tsx` for that separate,
|
|
234
|
+
// unresolved defect. `orDefault(height, 'auto')` is safe to share with
|
|
235
|
+
// `SkeletonIndicator` (unlike width's `'100%'`, deliberately NOT
|
|
236
|
+
// shared — see `emptyToUnset`): a PERCENTAGE height against an
|
|
237
|
+
// auto-height containing block computes to `auto` by spec regardless,
|
|
238
|
+
// so whether `sx.height` is omitted (falling through to
|
|
239
|
+
// `FallbackContainer`'s own `height: '100%'`) or set to the literal
|
|
240
|
+
// `'auto'` here, an unset-height container gives the same result
|
|
241
|
+
// either way — nothing for this fallback to protect against.
|
|
242
|
+
width: innerLength(theme, emptyToUnset(width)),
|
|
243
|
+
height: innerLength(theme, orDefault(height, 'auto')),
|
|
214
244
|
borderRadius: styleLength(theme, borderRadius),
|
|
215
245
|
}}
|
|
216
246
|
data-testid={`${testId}-fallback`}
|
|
@@ -242,8 +272,14 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
242
272
|
const { width, height, borderRadius, alt } = props;
|
|
243
273
|
const testId = props['data-testid'];
|
|
244
274
|
const metrics: BoxMetrics = {
|
|
245
|
-
|
|
246
|
-
|
|
275
|
+
// FUT-2774 #4 (see `emptyToUnset`): `metrics` also sizes the REAL `<img>`,
|
|
276
|
+
// where an unset width means "the image's own intrinsic size" —
|
|
277
|
+
// `brand-link.tsx`'s logo relies on exactly that. `height`'s `'auto'`
|
|
278
|
+
// fallback has no such live case to protect: a percentage height against
|
|
279
|
+
// an auto-height containing block computes to `auto` regardless, so
|
|
280
|
+
// sharing `SkeletonIndicator`'s fallback changes nothing observable.
|
|
281
|
+
width: innerLength(theme, emptyToUnset(width)),
|
|
282
|
+
height: innerLength(theme, orDefault(height, 'auto')),
|
|
247
283
|
objectFit: props.objectFit,
|
|
248
284
|
objectPosition: props.objectPosition,
|
|
249
285
|
borderRadius: styleLength(theme, borderRadius),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/LazyImage/LazyImage.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport CircularProgress from '@mui/material/CircularProgress/index.js';\nimport { styled, useTheme, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\nimport { Skeleton } from '../../layout/Skeleton';\nimport type { ResolvedLazyImageProps } from './LazyImage.hooks';\nimport { imgPassThrough, resolveLazyImageProps, useLazyImage } from './LazyImage.hooks';\nimport type { LazyImageProps } from './LazyImage.types';\nimport { sheen } from '../../../tokens/ink';\nimport { rem } from '../../../tokens/relative';\n\nconst ImageContainer = styled(Box)(() => ({\n position: 'relative',\n display: 'inline-block',\n overflow: 'hidden',\n}));\n\nconst StyledImage = styled('img')<{\n fadeIn?: boolean;\n fadeInDuration?: number;\n isLoaded?: boolean;\n}>(({ fadeIn, fadeInDuration = 300, isLoaded }) => ({\n display: 'block',\n maxWidth: '100%',\n height: 'auto',\n opacity: fadeIn ? (isLoaded ? 1 : 0) : 1,\n transition: fadeIn ? `opacity ${fadeInDuration}ms ease-in-out` : 'none',\n}));\n\nconst SpinnerOverlay = styled(Box)(({ theme }) => ({\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: sheen(theme, 0.8),\n borderRadius: '50%',\n padding: theme.spacing(1),\n}));\n\nconst FallbackContainer = styled(Box)(({ theme }) => ({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: theme.palette.action.disabledBackground,\n color: theme.palette.text.secondary,\n width: '100%',\n height: '100%',\n position: 'absolute',\n top: 0,\n left: 0,\n}));\n\ntype Length = number | string | undefined;\n\n/** A length prop as React `style` and MUI's `Skeleton` read it: a number is design px. */\nconst styleLength = (theme: Theme, value: Length): string | undefined =>\n typeof value === 'number' ? rem(theme, value) : value;\n\n/**\n * A width or height above 0 and up to 1 is a fraction of the container's parent, as in `sx`.\n * A negative number is no fraction: it stays a (negative, so invalid) length.\n */\nconst isFraction = (value: Length): value is number =>\n typeof value === 'number' && value > 0 && value <= 1;\n\n/**\n * A length prop as `sx` reads it on a width or height: a number of 1 or less is\n * a fraction of the parent, and that stays so; any other number is design px.\n */\nconst sxLength = (theme: Theme, value: Length): string | undefined =>\n isFraction(value) ? `${value * 100}%` : styleLength(theme, value);\n\n/**\n * A width or height for what is drawn inside the container. The container alone\n * takes a fraction, so everything in it fills it: a percentage here is measured\n * against the container and would stack (FUT-2666). Otherwise the same as `styleLength`.\n */\nconst innerLength = (theme: Theme, value: Length): string | undefined =>\n isFraction(value) ? '100%' : styleLength(theme, value);\n\n/**\n * A width or height with its default. Only an unset or empty size takes it: `0`\n * is a size, which `||` read as unset (FUT-2669), and `sx` reads `''` as `0%`.\n */\nconst orDefault = (value: Length, fallback: number | string): number | string =>\n value === undefined || value === '' ? fallback : value;\n\n/** The box the image occupies, as CSS, shared by the real image and every stand-in for it. */\ninterface BoxMetrics {\n width?: string;\n height?: string;\n objectFit: NonNullable<LazyImageProps['objectFit']>;\n objectPosition: string;\n borderRadius?: string;\n}\n\ninterface IndicatorProps {\n props: ResolvedLazyImageProps;\n metrics: BoxMetrics;\n}\n\nconst SkeletonIndicator: React.FC<IndicatorProps> = ({ props }) => {\n const theme = useTheme();\n return (\n <Skeleton\n variant=\"rectangular\"\n width={innerLength(theme, orDefault(props.width, '100%'))}\n height={innerLength(theme, orDefault(props.height, 'auto'))}\n animation={props.skeletonProps.animation || 'pulse'}\n intensity={props.skeletonProps.intensity}\n borderRadius={styleLength(theme, props.borderRadius)}\n data-testid={`${props['data-testid']}-skeleton`}\n />\n );\n};\n\nconst SpinnerIndicator: React.FC<IndicatorProps> = ({ props }) => {\n const theme = useTheme();\n const { spinnerProps } = props;\n\n return (\n <SpinnerOverlay>\n <CircularProgress\n size={styleLength(theme, spinnerProps.size || 40)}\n thickness={spinnerProps.thickness || 4}\n sx={{ color: spinnerProps.color || theme.palette.primary.main }}\n data-testid={`${props['data-testid']}-spinner`}\n />\n </SpinnerOverlay>\n );\n};\n\n/**\n * The placeholder sits in the box's flow and gives the box its size: the box's\n * own when one is set, the placeholder's when not. The real image is drawn over\n * it (`OVER_PLACEHOLDER`) until it has faded in. Its load and error are not the\n * image's, so neither reaches the caller; a failing one just retires.\n */\nconst PlaceholderIndicator: React.FC<IndicatorProps & { onError: () => void }> = ({\n props,\n metrics,\n onError,\n}) => {\n if (!props.placeholder) return null;\n\n return (\n <StyledImage\n src={props.placeholder}\n alt={`${props.alt} (loading)`}\n style={metrics}\n decoding={props.decoding}\n loading={props.lazy ? 'lazy' : props.loading}\n onError={onError}\n data-testid={`${props['data-testid']}-placeholder`}\n />\n );\n};\n\nconst LoadingIndicator: React.FC<\n IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>; onPlaceholderError: () => void }\n> = ({ kind, onPlaceholderError, ...rest }) => {\n switch (kind) {\n case 'skeleton':\n return <SkeletonIndicator {...rest} />;\n case 'spinner':\n return <SpinnerIndicator {...rest} />;\n case 'placeholder':\n return <PlaceholderIndicator {...rest} onError={onPlaceholderError} />;\n default:\n return null;\n }\n};\n\n/**\n * The real image while the placeholder is up: laid over it and cropped by\n * `objectFit`, so the box never grows to hold both. It takes the flow back\n * when the placeholder goes.\n */\nconst OVER_PLACEHOLDER: React.CSSProperties = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n};\n\nconst ErrorFallback: React.FC<IndicatorProps> = ({ props, metrics }) => {\n const theme = useTheme();\n const { alt, fallback, width, height, borderRadius } = props;\n const testId = props['data-testid'];\n\n if (!fallback) return null;\n\n if (typeof fallback === 'string') {\n return (\n <StyledImage\n src={fallback}\n alt={`${alt} (fallback)`}\n style={metrics}\n fadeIn={false}\n isLoaded={true}\n data-testid={`${testId}-fallback`}\n />\n );\n }\n\n return (\n <FallbackContainer\n sx={{\n width: innerLength(theme, width),\n height: innerLength(theme, height),\n borderRadius: styleLength(theme, borderRadius),\n }}\n data-testid={`${testId}-fallback`}\n >\n {fallback}\n </FallbackContainer>\n );\n};\n\n/**\n * LazyImage component with enhanced features for optimized image loading\n * Supports lazy loading, placeholders, error handling, and various loading states\n */\nexport const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps) {\n const props = resolveLazyImageProps(rawProps);\n const theme = useTheme();\n const {\n state,\n containerRef,\n effectiveLoadingState,\n handleImageLoad,\n handleImageError,\n handlePlaceholderError,\n showImage,\n showLoading,\n showPlaceholder,\n } = useLazyImage(props);\n\n const { width, height, borderRadius, alt } = props;\n const testId = props['data-testid'];\n const metrics: BoxMetrics = {\n width: innerLength(theme, width),\n height: innerLength(theme, height),\n objectFit: props.objectFit,\n objectPosition: props.objectPosition,\n borderRadius: styleLength(theme, borderRadius),\n };\n\n return (\n <ImageContainer\n ref={containerRef}\n className={props.className}\n sx={{\n width: sxLength(theme, orDefault(width, 'auto')),\n height: sxLength(theme, orDefault(height, 'auto')),\n // The same px length the image gets (`metrics`): in `sx` a bare number\n // would be a multiple of `shape.borderRadius`, and this box clips (FUT-2656).\n borderRadius: metrics.borderRadius,\n }}\n data-testid={testId}\n >\n {showLoading && (\n <LoadingIndicator\n kind={effectiveLoadingState}\n props={props}\n metrics={metrics}\n onPlaceholderError={handlePlaceholderError}\n />\n )}\n\n {showImage && (\n <StyledImage\n src={state.currentSrc || undefined}\n alt={alt}\n onLoad={handleImageLoad}\n onError={handleImageError}\n fadeIn={props.fadeIn}\n fadeInDuration={props.fadeInDuration}\n isLoaded={!state.isLoading}\n decoding={props.decoding}\n loading={props.lazy ? 'lazy' : props.loading}\n style={{ ...metrics, ...props.sx, ...(showPlaceholder && OVER_PLACEHOLDER) }}\n aria-label={props['aria-label'] || alt}\n aria-describedby={props['aria-describedby']}\n role={props.role}\n data-testid={`${testId}-img`}\n {...imgPassThrough(props)}\n />\n )}\n\n {state.hasError && <ErrorFallback props={props} metrics={metrics} />}\n </ImageContainer>\n );\n});\n\nLazyImage.displayName = 'LazyImage';\n"],"mappings":"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","names":["LazyImage"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/LazyImage/LazyImage.hooks.ts"],"sourcesContent":["import { useTheme } from '@mui/material/styles/index.js';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { remPx } from '../../../tokens/relative';\n\nimport type { LazyImageProps, LazyImageState } from './LazyImage.types';\n\ntype LazyImageDefaultedKeys =\n | 'height'\n | 'loadingState'\n | 'objectFit'\n | 'objectPosition'\n | 'lazy'\n | 'threshold'\n | 'fadeIn'\n | 'fadeInDuration'\n | 'retryOnError'\n | 'maxRetries'\n | 'retryDelay'\n | 'decoding'\n | 'skeletonProps'\n | 'spinnerProps'\n | 'sx'\n | 'role';\n\nexport type ResolvedLazyImageProps = LazyImageProps &\n Required<Pick<LazyImageProps, LazyImageDefaultedKeys>>;\n\nconst LAZY_IMAGE_DEFAULTS: Pick<LazyImageProps, LazyImageDefaultedKeys> = {\n height: 'auto',\n loadingState: 'skeleton',\n objectFit: 'cover',\n objectPosition: 'center',\n lazy: true,\n threshold: 0,\n fadeIn: true,\n fadeInDuration: 300,\n retryOnError: false,\n maxRetries: 3,\n retryDelay: 1000,\n decoding: 'async',\n skeletonProps: {},\n spinnerProps: {},\n sx: {},\n role: 'img',\n};\n\n// Strips explicitly-undefined props before the merge, so `lazy={undefined}` still\n// falls back to the default exactly as a destructuring default would.\nconst definedProps = (props: LazyImageProps): Partial<LazyImageProps> =>\n Object.fromEntries(\n Object.entries(props).filter(([, value]) => value !== undefined),\n ) as Partial<LazyImageProps>;\n\nexport const resolveLazyImageProps = (props: LazyImageProps): ResolvedLazyImageProps =>\n ({ ...LAZY_IMAGE_DEFAULTS, ...definedProps(props) }) as ResolvedLazyImageProps;\n\n// Props this component consumes itself. Anything else the caller passed is an\n// ordinary <img> attribute and is forwarded untouched.\nconst OWN_PROPS = new Set([\n 'src', 'alt', 'width', 'height', 'placeholder', 'fallback', 'loadingState',\n 'showSpinner', 'objectFit', 'objectPosition', 'borderRadius', 'lazy', 'rootMargin',\n 'threshold', 'fadeIn', 'fadeInDuration', 'onLoad', 'onError', 'onLoadStart',\n 'retryOnError', 'maxRetries', 'retryDelay', 'decoding', 'loading', 'fetchPriority',\n 'skeletonProps', 'spinnerProps', 'sx', 'className', 'data-testid', 'aria-label',\n 'aria-describedby', 'role',\n]);\n\nexport const imgPassThrough = (props: ResolvedLazyImageProps): Record<string, unknown> =>\n Object.fromEntries(Object.entries(props).filter(([key]) => !OWN_PROPS.has(key)));\n\n/**\n * Flips `isVisible` the first time the container nears the viewport, then stops\n * observing. Does nothing when `lazy` is off — the caller wants the request now.\n */\nconst useVisibility = (\n props: ResolvedLazyImageProps,\n containerRef: React.RefObject<HTMLDivElement | null>,\n isVisible: boolean,\n onVisible: () => void,\n) => {\n const { lazy, src, threshold, onLoadStart } = props;\n const theme = useTheme();\n // `IntersectionObserver` takes px or %, never rem: the default reach is 100\n // design px, measured through the theme so it moves with the root.\n const rootMargin = props.rootMargin ?? `${remPx(theme, 100)}px`;\n\n useEffect(() => {\n if (!lazy || isVisible) {\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n onVisible();\n onLoadStart?.();\n observer.unobserve(entry.target);\n }\n });\n },\n { rootMargin, threshold },\n );\n\n if (containerRef.current) {\n observer.observe(containerRef.current);\n }\n\n return () => {\n if (containerRef.current) {\n observer.unobserve(containerRef.current);\n }\n };\n }, [lazy, src, rootMargin, threshold, isVisible, onLoadStart, onVisible, containerRef]);\n};\n\n/** A pending timer has to be cancelled on unmount, or it sets state on a dead component. */\nconst useTimeoutRef = () => {\n const ref = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n useEffect(\n () => () => {\n if (ref.current) {\n clearTimeout(ref.current);\n }\n },\n [],\n );\n\n return ref;\n};\n\n/**\n * Whether the placeholder image is up. It is a loading state: only\n * `loadingState=\"placeholder\"` with a `placeholder` set draws it, from mount\n * until the real image settles — its `load` plus the fade, or its final error.\n * A placeholder that fails to load retires at once, as if none were set.\n */\nconst usePlaceholderPhase = (\n props: ResolvedLazyImageProps,\n loadingState: NonNullable<LazyImageProps['loadingState']>,\n) => {\n const { placeholder, fadeIn, fadeInDuration } = props;\n // Only this mode draws a placeholder. Otherwise the real image settling\n // retires the phase at once — batched into the load's own render, with no\n // timer — so a placeholder switched on afterwards never covers a loaded image.\n const active = loadingState === 'placeholder' && Boolean(placeholder);\n const [retired, setRetired] = useState(false);\n const fadeTimeoutRef = useTimeoutRef();\n\n const retire = useCallback(() => setRetired(true), []);\n\n // The real image fades in over the placeholder, so it goes when the fade ends.\n const retireAfterFade = useCallback(() => {\n if (!active || !fadeIn) {\n setRetired(true);\n return;\n }\n clearTimeout(fadeTimeoutRef.current);\n fadeTimeoutRef.current = setTimeout(() => setRetired(true), fadeInDuration);\n }, [active, fadeIn, fadeInDuration, fadeTimeoutRef]);\n\n return {\n showPlaceholder: active && !retired,\n retirePlaceholder: retire,\n retirePlaceholderAfterFade: retireAfterFade,\n };\n};\n\n/**\n * The real image's state, and when it is requested: at once when not lazy,\n * else the first time the container nears the viewport. The main <img> only\n * ever carries the real src — a placeholder is drawn by its own element, so its\n * load and error are never the image's.\n */\nconst useImageSource = (\n props: ResolvedLazyImageProps,\n containerRef: React.RefObject<HTMLDivElement | null>,\n) => {\n const { src, lazy } = props;\n const [state, setState] = useState<LazyImageState>({\n isLoading: true,\n hasError: false,\n isVisible: !lazy, // If not lazy, load immediately\n retryCount: 0,\n currentSrc: lazy ? null : src,\n });\n\n const markVisible = useCallback(\n () => setState((prev) => ({ ...prev, isVisible: true, currentSrc: src })),\n [src],\n );\n useVisibility(props, containerRef, state.isVisible, markVisible);\n\n // Load image when it becomes visible\n useEffect(() => {\n if (!state.isVisible || !src || state.currentSrc === src) {\n return;\n }\n\n setState((prev) => ({ ...prev, currentSrc: src, isLoading: true, hasError: false }));\n }, [state.isVisible, src, state.currentSrc]);\n\n return [state, setState] as const;\n};\n\n/**\n * Owns whether the real image has been requested yet and how it went: the\n * observer above, the load/error handlers, the retry timer, and how long the\n * placeholder stays up.\n */\nexport const useLazyImage = (props: ResolvedLazyImageProps) => {\n const { src, loadingState, showSpinner } = props;\n const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const retryTimeoutRef = useTimeoutRef();\n const [state, setState] = useImageSource(props, containerRef);\n\n // Handle deprecated showSpinner prop\n const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;\n const { showPlaceholder, retirePlaceholder, retirePlaceholderAfterFade } = usePlaceholderPhase(\n props,\n effectiveLoadingState,\n );\n\n const handleImageLoad = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement>) => {\n setState((prev) => ({ ...prev, isLoading: false, hasError: false, retryCount: 0 }));\n retirePlaceholderAfterFade();\n onLoad?.(event);\n },\n [onLoad, retirePlaceholderAfterFade],\n );\n\n const handleImageError = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement>) => {\n if (!retryOnError || state.retryCount >= maxRetries) {\n setState((prev) => ({ ...prev, isLoading: false, hasError: true }));\n retirePlaceholder();\n onError?.(event);\n return;\n }\n\n retryTimeoutRef.current = setTimeout(() => {\n setState((prev) => ({\n ...prev,\n retryCount: prev.retryCount + 1,\n currentSrc: `${src}?retry=${prev.retryCount + 1}`, // Force reload with cache buster\n }));\n }, retryDelay);\n },\n [onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder],\n );\n\n return {\n state,\n containerRef,\n effectiveLoadingState,\n handleImageLoad,\n handleImageError,\n handlePlaceholderError: retirePlaceholder,\n showImage: Boolean(state.currentSrc) && !state.hasError,\n // In placeholder mode the placeholder outlasts `isLoading`: it stays under\n // the real image until the fade has ended.\n showLoading: effectiveLoadingState === 'placeholder' ? showPlaceholder : state.isLoading,\n showPlaceholder,\n };\n};\n"],"mappings":"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","names":[]}
|