@12-apps/ui 6.39.5 → 6.39.7
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-SDK5LPPC.js → chunk-BVF2BSWV.js} +25 -13
- package/dist/chunk-BVF2BSWV.js.map +1 -0
- package/dist/{chunk-HCT5ELST.js → chunk-GMUYCN33.js} +47 -12
- package/dist/chunk-GMUYCN33.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 +4 -1
- package/dist/types/components/data-display/LazyImage/LazyImage.hooks.d.ts.map +1 -1
- package/dist/types/components/data-display/LazyImage/LazyImage.types.d.ts +19 -3
- package/dist/types/components/data-display/LazyImage/LazyImage.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/LazyImage/LazyImage.hooks.ts +78 -16
- package/src/components/data-display/LazyImage/LazyImage.md +23 -3
- package/src/components/data-display/LazyImage/LazyImage.tsx +42 -14
- package/src/components/data-display/LazyImage/LazyImage.types.ts +19 -3
- package/dist/chunk-HCT5ELST.js.map +0 -1
- package/dist/chunk-SDK5LPPC.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LazyImage
|
|
3
|
-
} from "../../../../chunk-
|
|
4
|
-
import "../../../../chunk-
|
|
3
|
+
} from "../../../../chunk-BVF2BSWV.js";
|
|
4
|
+
import "../../../../chunk-GMUYCN33.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-BVF2BSWV.js";
|
|
95
|
+
import "../../../chunk-GMUYCN33.js";
|
|
96
96
|
import "../../../chunk-724ZONOL.js";
|
|
97
97
|
import "../../../chunk-UM35GGCS.js";
|
|
98
98
|
import {
|
|
@@ -477,8 +477,8 @@ import "../../chunk-S5UY5U73.js";
|
|
|
477
477
|
import "../../chunk-5ZFO7ROE.js";
|
|
478
478
|
import {
|
|
479
479
|
LazyImage
|
|
480
|
-
} from "../../chunk-
|
|
481
|
-
import "../../chunk-
|
|
480
|
+
} from "../../chunk-BVF2BSWV.js";
|
|
481
|
+
import "../../chunk-GMUYCN33.js";
|
|
482
482
|
import "../../chunk-724ZONOL.js";
|
|
483
483
|
import "../../chunk-UM35GGCS.js";
|
|
484
484
|
import {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
imgPassThrough,
|
|
3
3
|
resolveLazyImageProps,
|
|
4
4
|
useLazyImage
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GMUYCN33.js";
|
|
6
6
|
import {
|
|
7
7
|
Skeleton
|
|
8
8
|
} from "./chunk-BEGTLELZ.js";
|
|
@@ -59,14 +59,15 @@ var styleLength = (theme, value) => typeof value === "number" ? rem(theme, value
|
|
|
59
59
|
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
|
+
var orDefault = (value, fallback) => value === void 0 || value === "" ? fallback : value;
|
|
62
63
|
var SkeletonIndicator = ({ props }) => {
|
|
63
64
|
const theme = useTheme();
|
|
64
65
|
return /* @__PURE__ */ jsx(
|
|
65
66
|
Skeleton,
|
|
66
67
|
{
|
|
67
68
|
variant: "rectangular",
|
|
68
|
-
width: innerLength(theme, props.width
|
|
69
|
-
height: innerLength(theme, props.height
|
|
69
|
+
width: innerLength(theme, orDefault(props.width, "100%")),
|
|
70
|
+
height: innerLength(theme, orDefault(props.height, "auto")),
|
|
70
71
|
animation: props.skeletonProps.animation || "pulse",
|
|
71
72
|
intensity: props.skeletonProps.intensity,
|
|
72
73
|
borderRadius: styleLength(theme, props.borderRadius),
|
|
@@ -90,31 +91,40 @@ var SpinnerIndicator = ({ props }) => {
|
|
|
90
91
|
var PlaceholderIndicator = ({
|
|
91
92
|
props,
|
|
92
93
|
metrics,
|
|
93
|
-
|
|
94
|
+
onError
|
|
94
95
|
}) => {
|
|
95
|
-
if (!props.placeholder
|
|
96
|
+
if (!props.placeholder) return null;
|
|
96
97
|
return /* @__PURE__ */ jsx(
|
|
97
98
|
StyledImage,
|
|
98
99
|
{
|
|
99
100
|
src: props.placeholder,
|
|
100
101
|
alt: `${props.alt} (loading)`,
|
|
101
102
|
style: metrics,
|
|
103
|
+
decoding: props.decoding,
|
|
104
|
+
loading: props.lazy ? "lazy" : props.loading,
|
|
105
|
+
onError,
|
|
102
106
|
"data-testid": `${props["data-testid"]}-placeholder`
|
|
103
107
|
}
|
|
104
108
|
);
|
|
105
109
|
};
|
|
106
|
-
var LoadingIndicator = ({ kind,
|
|
110
|
+
var LoadingIndicator = ({ kind, onPlaceholderError, ...rest }) => {
|
|
107
111
|
switch (kind) {
|
|
108
112
|
case "skeleton":
|
|
109
113
|
return /* @__PURE__ */ jsx(SkeletonIndicator, { ...rest });
|
|
110
114
|
case "spinner":
|
|
111
115
|
return /* @__PURE__ */ jsx(SpinnerIndicator, { ...rest });
|
|
112
116
|
case "placeholder":
|
|
113
|
-
return /* @__PURE__ */ jsx(PlaceholderIndicator, { ...rest,
|
|
117
|
+
return /* @__PURE__ */ jsx(PlaceholderIndicator, { ...rest, onError: onPlaceholderError });
|
|
114
118
|
default:
|
|
115
119
|
return null;
|
|
116
120
|
}
|
|
117
121
|
};
|
|
122
|
+
var OVER_PLACEHOLDER = {
|
|
123
|
+
position: "absolute",
|
|
124
|
+
inset: 0,
|
|
125
|
+
width: "100%",
|
|
126
|
+
height: "100%"
|
|
127
|
+
};
|
|
118
128
|
var ErrorFallback = ({ props, metrics }) => {
|
|
119
129
|
const theme = useTheme();
|
|
120
130
|
const { alt, fallback, width, height, borderRadius } = props;
|
|
@@ -155,8 +165,10 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
155
165
|
effectiveLoadingState,
|
|
156
166
|
handleImageLoad,
|
|
157
167
|
handleImageError,
|
|
168
|
+
handlePlaceholderError,
|
|
158
169
|
showImage,
|
|
159
|
-
showLoading
|
|
170
|
+
showLoading,
|
|
171
|
+
showPlaceholder
|
|
160
172
|
} = useLazyImage(props);
|
|
161
173
|
const { width, height, borderRadius, alt } = props;
|
|
162
174
|
const testId = props["data-testid"];
|
|
@@ -173,8 +185,8 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
173
185
|
ref: containerRef,
|
|
174
186
|
className: props.className,
|
|
175
187
|
sx: {
|
|
176
|
-
width: sxLength(theme, width
|
|
177
|
-
height: sxLength(theme, height
|
|
188
|
+
width: sxLength(theme, orDefault(width, "auto")),
|
|
189
|
+
height: sxLength(theme, orDefault(height, "auto")),
|
|
178
190
|
// The same px length the image gets (`metrics`): in `sx` a bare number
|
|
179
191
|
// would be a multiple of `shape.borderRadius`, and this box clips (FUT-2656).
|
|
180
192
|
borderRadius: metrics.borderRadius
|
|
@@ -187,7 +199,7 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
187
199
|
kind: effectiveLoadingState,
|
|
188
200
|
props,
|
|
189
201
|
metrics,
|
|
190
|
-
|
|
202
|
+
onPlaceholderError: handlePlaceholderError
|
|
191
203
|
}
|
|
192
204
|
),
|
|
193
205
|
showImage && /* @__PURE__ */ jsx(
|
|
@@ -202,7 +214,7 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
202
214
|
isLoaded: !state.isLoading,
|
|
203
215
|
decoding: props.decoding,
|
|
204
216
|
loading: props.lazy ? "lazy" : props.loading,
|
|
205
|
-
style: { ...metrics, ...props.sx },
|
|
217
|
+
style: { ...metrics, ...props.sx, ...showPlaceholder && OVER_PLACEHOLDER },
|
|
206
218
|
"aria-label": props["aria-label"] || alt,
|
|
207
219
|
"aria-describedby": props["aria-describedby"],
|
|
208
220
|
role: props.role,
|
|
@@ -220,4 +232,4 @@ LazyImage.displayName = "LazyImage";
|
|
|
220
232
|
export {
|
|
221
233
|
LazyImage
|
|
222
234
|
};
|
|
223
|
-
//# sourceMappingURL=chunk-
|
|
235
|
+
//# sourceMappingURL=chunk-BVF2BSWV.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/** 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"]}
|
|
@@ -93,7 +93,7 @@ var useVisibility = (props, containerRef, isVisible, onVisible) => {
|
|
|
93
93
|
};
|
|
94
94
|
}, [lazy, src, rootMargin, threshold, isVisible, onLoadStart, onVisible, containerRef]);
|
|
95
95
|
};
|
|
96
|
-
var
|
|
96
|
+
var useTimeoutRef = () => {
|
|
97
97
|
const ref = useRef(void 0);
|
|
98
98
|
useEffect(
|
|
99
99
|
() => () => {
|
|
@@ -105,19 +105,35 @@ var useRetryTimeout = () => {
|
|
|
105
105
|
);
|
|
106
106
|
return ref;
|
|
107
107
|
};
|
|
108
|
-
var
|
|
109
|
-
const {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
108
|
+
var usePlaceholderPhase = (props, loadingState) => {
|
|
109
|
+
const { placeholder, fadeIn, fadeInDuration } = props;
|
|
110
|
+
const active = loadingState === "placeholder" && Boolean(placeholder);
|
|
111
|
+
const [retired, setRetired] = useState(false);
|
|
112
|
+
const fadeTimeoutRef = useTimeoutRef();
|
|
113
|
+
const retire = useCallback(() => setRetired(true), []);
|
|
114
|
+
const retireAfterFade = useCallback(() => {
|
|
115
|
+
if (!active || !fadeIn) {
|
|
116
|
+
setRetired(true);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
clearTimeout(fadeTimeoutRef.current);
|
|
120
|
+
fadeTimeoutRef.current = setTimeout(() => setRetired(true), fadeInDuration);
|
|
121
|
+
}, [active, fadeIn, fadeInDuration, fadeTimeoutRef]);
|
|
122
|
+
return {
|
|
123
|
+
showPlaceholder: active && !retired,
|
|
124
|
+
retirePlaceholder: retire,
|
|
125
|
+
retirePlaceholderAfterFade: retireAfterFade
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
var useImageSource = (props, containerRef) => {
|
|
129
|
+
const { src, lazy } = props;
|
|
114
130
|
const [state, setState] = useState({
|
|
115
131
|
isLoading: true,
|
|
116
132
|
hasError: false,
|
|
117
133
|
isVisible: !lazy,
|
|
118
134
|
// If not lazy, load immediately
|
|
119
135
|
retryCount: 0,
|
|
120
|
-
currentSrc: lazy ?
|
|
136
|
+
currentSrc: lazy ? null : src
|
|
121
137
|
});
|
|
122
138
|
const markVisible = useCallback(
|
|
123
139
|
() => setState((prev) => ({ ...prev, isVisible: true, currentSrc: src })),
|
|
@@ -130,17 +146,32 @@ var useLazyImage = (props) => {
|
|
|
130
146
|
}
|
|
131
147
|
setState((prev) => ({ ...prev, currentSrc: src, isLoading: true, hasError: false }));
|
|
132
148
|
}, [state.isVisible, src, state.currentSrc]);
|
|
149
|
+
return [state, setState];
|
|
150
|
+
};
|
|
151
|
+
var useLazyImage = (props) => {
|
|
152
|
+
const { src, loadingState, showSpinner } = props;
|
|
153
|
+
const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
|
|
154
|
+
const containerRef = useRef(null);
|
|
155
|
+
const retryTimeoutRef = useTimeoutRef();
|
|
156
|
+
const [state, setState] = useImageSource(props, containerRef);
|
|
157
|
+
const effectiveLoadingState = showSpinner ? "spinner" : loadingState;
|
|
158
|
+
const { showPlaceholder, retirePlaceholder, retirePlaceholderAfterFade } = usePlaceholderPhase(
|
|
159
|
+
props,
|
|
160
|
+
effectiveLoadingState
|
|
161
|
+
);
|
|
133
162
|
const handleImageLoad = useCallback(
|
|
134
163
|
(event) => {
|
|
135
164
|
setState((prev) => ({ ...prev, isLoading: false, hasError: false, retryCount: 0 }));
|
|
165
|
+
retirePlaceholderAfterFade();
|
|
136
166
|
onLoad?.(event);
|
|
137
167
|
},
|
|
138
|
-
[onLoad]
|
|
168
|
+
[onLoad, retirePlaceholderAfterFade]
|
|
139
169
|
);
|
|
140
170
|
const handleImageError = useCallback(
|
|
141
171
|
(event) => {
|
|
142
172
|
if (!retryOnError || state.retryCount >= maxRetries) {
|
|
143
173
|
setState((prev) => ({ ...prev, isLoading: false, hasError: true }));
|
|
174
|
+
retirePlaceholder();
|
|
144
175
|
onError?.(event);
|
|
145
176
|
return;
|
|
146
177
|
}
|
|
@@ -153,7 +184,7 @@ var useLazyImage = (props) => {
|
|
|
153
184
|
}));
|
|
154
185
|
}, retryDelay);
|
|
155
186
|
},
|
|
156
|
-
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src]
|
|
187
|
+
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder]
|
|
157
188
|
);
|
|
158
189
|
return {
|
|
159
190
|
state,
|
|
@@ -161,8 +192,12 @@ var useLazyImage = (props) => {
|
|
|
161
192
|
effectiveLoadingState,
|
|
162
193
|
handleImageLoad,
|
|
163
194
|
handleImageError,
|
|
195
|
+
handlePlaceholderError: retirePlaceholder,
|
|
164
196
|
showImage: Boolean(state.currentSrc) && !state.hasError,
|
|
165
|
-
|
|
197
|
+
// In placeholder mode the placeholder outlasts `isLoading`: it stays under
|
|
198
|
+
// the real image until the fade has ended.
|
|
199
|
+
showLoading: effectiveLoadingState === "placeholder" ? showPlaceholder : state.isLoading,
|
|
200
|
+
showPlaceholder
|
|
166
201
|
};
|
|
167
202
|
};
|
|
168
203
|
|
|
@@ -171,4 +206,4 @@ export {
|
|
|
171
206
|
imgPassThrough,
|
|
172
207
|
useLazyImage
|
|
173
208
|
};
|
|
174
|
-
//# sourceMappingURL=chunk-
|
|
209
|
+
//# sourceMappingURL=chunk-GMUYCN33.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\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":[]}
|
|
@@ -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-BVF2BSWV.js";
|
|
5
|
+
import "../chunk-GMUYCN33.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;AAuNxD;;;GAGG;AACH,eAAO,MAAM,SAAS,4CAsEpB,CAAC"}
|
|
@@ -6,7 +6,8 @@ export declare const resolveLazyImageProps: (props: LazyImageProps) => ResolvedL
|
|
|
6
6
|
export declare const imgPassThrough: (props: ResolvedLazyImageProps) => Record<string, unknown>;
|
|
7
7
|
/**
|
|
8
8
|
* Owns whether the real image has been requested yet and how it went: the
|
|
9
|
-
* observer above, the load/error handlers,
|
|
9
|
+
* observer above, the load/error handlers, the retry timer, and how long the
|
|
10
|
+
* placeholder stays up.
|
|
10
11
|
*/
|
|
11
12
|
export declare const useLazyImage: (props: ResolvedLazyImageProps) => {
|
|
12
13
|
state: LazyImageState;
|
|
@@ -14,8 +15,10 @@ export declare const useLazyImage: (props: ResolvedLazyImageProps) => {
|
|
|
14
15
|
effectiveLoadingState: "placeholder" | "none" | "spinner" | "skeleton";
|
|
15
16
|
handleImageLoad: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
16
17
|
handleImageError: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
18
|
+
handlePlaceholderError: () => void;
|
|
17
19
|
showImage: boolean;
|
|
18
20
|
showLoading: boolean;
|
|
21
|
+
showPlaceholder: boolean;
|
|
19
22
|
};
|
|
20
23
|
export {};
|
|
21
24
|
//# sourceMappingURL=LazyImage.hooks.d.ts.map
|
|
@@ -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;AAEX,MAAM,MAAM,sBAAsB,GAAG,cAAc,GACjD,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,sBAAsB,CAAC,CAAC,CAAC;AA4BzD,eAAO,MAAM,qBAAqB,GAAI,OAAO,cAAc,KAAG,sBACkB,CAAC;AAajF,eAAO,MAAM,cAAc,GAAI,OAAO,sBAAsB,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,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,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAAG,cAAc,GACjD,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,sBAAsB,CAAC,CAAC,CAAC;AA4BzD,eAAO,MAAM,qBAAqB,GAAI,OAAO,cAAc,KAAG,sBACkB,CAAC;AAajF,eAAO,MAAM,cAAc,GAAI,OAAO,sBAAsB,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACH,CAAC;AA0InF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,sBAAsB;;;;6BAgB9C,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;8BAStC,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;;;;;CAgCjD,CAAC"}
|
|
@@ -28,7 +28,17 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
28
28
|
*/
|
|
29
29
|
height?: number | string;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* A low-resolution image URL shown while the real image loads. Only
|
|
32
|
+
* `loadingState="placeholder"` draws it; under any other loading state it is
|
|
33
|
+
* ignored. It shows from mount, lazy or not, until the real image has loaded
|
|
34
|
+
* and faded in (at once when `fadeIn` is off) or has finally failed. The real
|
|
35
|
+
* image is drawn over it and cropped by `objectFit`, so the box takes the
|
|
36
|
+
* placeholder's shape until the fade ends. A placeholder that fails to load is
|
|
37
|
+
* dropped, as if none were set, and is never reported to `onError`; neither
|
|
38
|
+
* does its load reach `onLoad`.
|
|
39
|
+
*
|
|
40
|
+
* Give LazyImage a `width` (or `"100%"`) in placeholder mode: with none, the
|
|
41
|
+
* placeholder shows at its own, usually tiny, size.
|
|
32
42
|
*/
|
|
33
43
|
placeholder?: string;
|
|
34
44
|
/**
|
|
@@ -39,8 +49,12 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
39
49
|
* Loading state type
|
|
40
50
|
* - 'skeleton': Shows a skeleton loader
|
|
41
51
|
* - 'spinner': Shows a loading spinner
|
|
42
|
-
* - 'placeholder': Shows placeholder image
|
|
52
|
+
* - 'placeholder': Shows the `placeholder` image, in the box's flow, until the
|
|
53
|
+
* real image has faded in over it; nothing when no `placeholder` is set.
|
|
54
|
+
* Set a `width` (or `"100%"`), or the placeholder shows at its own size.
|
|
43
55
|
* - 'none': No loading indicator
|
|
56
|
+
*
|
|
57
|
+
* `placeholder` applies only to 'placeholder'.
|
|
44
58
|
* @default 'skeleton'
|
|
45
59
|
*/
|
|
46
60
|
loadingState?: LazyImageLoadingState;
|
|
@@ -151,7 +165,9 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
151
165
|
* Inline CSS for the `<img>` (its `style`), not MUI's `sx`: theme values and
|
|
152
166
|
* nested selectors such as `'&:hover'` do not apply. The image's box clips
|
|
153
167
|
* anything painted outside the image, such as a shadow, so put those on a
|
|
154
|
-
* wrapping element.
|
|
168
|
+
* wrapping element. While a placeholder is up (`loadingState="placeholder"`),
|
|
169
|
+
* the image is laid over it, so its `position`, `inset`, `width` and `height`
|
|
170
|
+
* are fixed until the placeholder retires; the rest applies throughout.
|
|
155
171
|
*/
|
|
156
172
|
sx?: React.CSSProperties;
|
|
157
173
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LazyImage.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;AACtF,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAC;AAEpF,MAAM,WAAW,cAAe,SAAQ,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC;IAC/E;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzB
|
|
1
|
+
{"version":3,"file":"LazyImage.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/LazyImage/LazyImage.types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -115,8 +115,8 @@ const useVisibility = (
|
|
|
115
115
|
}, [lazy, src, rootMargin, threshold, isVisible, onLoadStart, onVisible, containerRef]);
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
/** A pending
|
|
119
|
-
const
|
|
118
|
+
/** A pending timer has to be cancelled on unmount, or it sets state on a dead component. */
|
|
119
|
+
const useTimeoutRef = () => {
|
|
120
120
|
const ref = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
121
121
|
|
|
122
122
|
useEffect(
|
|
@@ -132,25 +132,59 @@ const useRetryTimeout = () => {
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* Whether the placeholder image is up. It is a loading state: only
|
|
136
|
+
* `loadingState="placeholder"` with a `placeholder` set draws it, from mount
|
|
137
|
+
* until the real image settles — its `load` plus the fade, or its final error.
|
|
138
|
+
* A placeholder that fails to load retires at once, as if none were set.
|
|
137
139
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const usePlaceholderPhase = (
|
|
141
|
+
props: ResolvedLazyImageProps,
|
|
142
|
+
loadingState: NonNullable<LazyImageProps['loadingState']>,
|
|
143
|
+
) => {
|
|
144
|
+
const { placeholder, fadeIn, fadeInDuration } = props;
|
|
145
|
+
// Only this mode draws a placeholder. Otherwise the real image settling
|
|
146
|
+
// retires the phase at once — batched into the load's own render, with no
|
|
147
|
+
// timer — so a placeholder switched on afterwards never covers a loaded image.
|
|
148
|
+
const active = loadingState === 'placeholder' && Boolean(placeholder);
|
|
149
|
+
const [retired, setRetired] = useState(false);
|
|
150
|
+
const fadeTimeoutRef = useTimeoutRef();
|
|
141
151
|
|
|
142
|
-
const
|
|
143
|
-
const retryTimeoutRef = useRetryTimeout();
|
|
152
|
+
const retire = useCallback(() => setRetired(true), []);
|
|
144
153
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
154
|
+
// The real image fades in over the placeholder, so it goes when the fade ends.
|
|
155
|
+
const retireAfterFade = useCallback(() => {
|
|
156
|
+
if (!active || !fadeIn) {
|
|
157
|
+
setRetired(true);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
clearTimeout(fadeTimeoutRef.current);
|
|
161
|
+
fadeTimeoutRef.current = setTimeout(() => setRetired(true), fadeInDuration);
|
|
162
|
+
}, [active, fadeIn, fadeInDuration, fadeTimeoutRef]);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
showPlaceholder: active && !retired,
|
|
166
|
+
retirePlaceholder: retire,
|
|
167
|
+
retirePlaceholderAfterFade: retireAfterFade,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
147
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The real image's state, and when it is requested: at once when not lazy,
|
|
173
|
+
* else the first time the container nears the viewport. The main <img> only
|
|
174
|
+
* ever carries the real src — a placeholder is drawn by its own element, so its
|
|
175
|
+
* load and error are never the image's.
|
|
176
|
+
*/
|
|
177
|
+
const useImageSource = (
|
|
178
|
+
props: ResolvedLazyImageProps,
|
|
179
|
+
containerRef: React.RefObject<HTMLDivElement | null>,
|
|
180
|
+
) => {
|
|
181
|
+
const { src, lazy } = props;
|
|
148
182
|
const [state, setState] = useState<LazyImageState>({
|
|
149
183
|
isLoading: true,
|
|
150
184
|
hasError: false,
|
|
151
185
|
isVisible: !lazy, // If not lazy, load immediately
|
|
152
186
|
retryCount: 0,
|
|
153
|
-
currentSrc: lazy ?
|
|
187
|
+
currentSrc: lazy ? null : src,
|
|
154
188
|
});
|
|
155
189
|
|
|
156
190
|
const markVisible = useCallback(
|
|
@@ -168,18 +202,43 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
168
202
|
setState((prev) => ({ ...prev, currentSrc: src, isLoading: true, hasError: false }));
|
|
169
203
|
}, [state.isVisible, src, state.currentSrc]);
|
|
170
204
|
|
|
205
|
+
return [state, setState] as const;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Owns whether the real image has been requested yet and how it went: the
|
|
210
|
+
* observer above, the load/error handlers, the retry timer, and how long the
|
|
211
|
+
* placeholder stays up.
|
|
212
|
+
*/
|
|
213
|
+
export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
214
|
+
const { src, loadingState, showSpinner } = props;
|
|
215
|
+
const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
|
|
216
|
+
|
|
217
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
218
|
+
const retryTimeoutRef = useTimeoutRef();
|
|
219
|
+
const [state, setState] = useImageSource(props, containerRef);
|
|
220
|
+
|
|
221
|
+
// Handle deprecated showSpinner prop
|
|
222
|
+
const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;
|
|
223
|
+
const { showPlaceholder, retirePlaceholder, retirePlaceholderAfterFade } = usePlaceholderPhase(
|
|
224
|
+
props,
|
|
225
|
+
effectiveLoadingState,
|
|
226
|
+
);
|
|
227
|
+
|
|
171
228
|
const handleImageLoad = useCallback(
|
|
172
229
|
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
|
173
230
|
setState((prev) => ({ ...prev, isLoading: false, hasError: false, retryCount: 0 }));
|
|
231
|
+
retirePlaceholderAfterFade();
|
|
174
232
|
onLoad?.(event);
|
|
175
233
|
},
|
|
176
|
-
[onLoad],
|
|
234
|
+
[onLoad, retirePlaceholderAfterFade],
|
|
177
235
|
);
|
|
178
236
|
|
|
179
237
|
const handleImageError = useCallback(
|
|
180
238
|
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
|
181
239
|
if (!retryOnError || state.retryCount >= maxRetries) {
|
|
182
240
|
setState((prev) => ({ ...prev, isLoading: false, hasError: true }));
|
|
241
|
+
retirePlaceholder();
|
|
183
242
|
onError?.(event);
|
|
184
243
|
return;
|
|
185
244
|
}
|
|
@@ -192,7 +251,7 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
192
251
|
}));
|
|
193
252
|
}, retryDelay);
|
|
194
253
|
},
|
|
195
|
-
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src],
|
|
254
|
+
[onError, retryOnError, state.retryCount, maxRetries, retryDelay, src, retirePlaceholder],
|
|
196
255
|
);
|
|
197
256
|
|
|
198
257
|
return {
|
|
@@ -201,8 +260,11 @@ export const useLazyImage = (props: ResolvedLazyImageProps) => {
|
|
|
201
260
|
effectiveLoadingState,
|
|
202
261
|
handleImageLoad,
|
|
203
262
|
handleImageError,
|
|
263
|
+
handlePlaceholderError: retirePlaceholder,
|
|
204
264
|
showImage: Boolean(state.currentSrc) && !state.hasError,
|
|
205
|
-
|
|
206
|
-
|
|
265
|
+
// In placeholder mode the placeholder outlasts `isLoading`: it stays under
|
|
266
|
+
// the real image until the fade has ended.
|
|
267
|
+
showLoading: effectiveLoadingState === 'placeholder' ? showPlaceholder : state.isLoading,
|
|
268
|
+
showPlaceholder,
|
|
207
269
|
};
|
|
208
270
|
};
|
|
@@ -43,6 +43,24 @@ function App() {
|
|
|
43
43
|
/>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
The placeholder is a loading state, and only `loadingState="placeholder"` draws
|
|
47
|
+
it — under `skeleton`, `spinner` or `none` the `placeholder` prop is ignored.
|
|
48
|
+
|
|
49
|
+
- It shows from mount, lazy or not, until the real image settles: its `load`
|
|
50
|
+
plus `fadeInDuration` (at once when `fadeIn` is off), or its final error once
|
|
51
|
+
any retries are spent.
|
|
52
|
+
- It sits in the box's flow and gives the box its size. The real image is drawn
|
|
53
|
+
over it, cropped by `objectFit`, and fades in on top; when the placeholder
|
|
54
|
+
goes, the real image takes the flow. So a placeholder whose shape differs
|
|
55
|
+
from the real image's gives the box its shape until the fade ends.
|
|
56
|
+
- A placeholder that fails to load is dropped at once, as if none were set.
|
|
57
|
+
- `onLoad` and `onError` are the real image's only: the placeholder's load
|
|
58
|
+
never calls `onLoad`, and its failure never calls `onError`.
|
|
59
|
+
- An empty `src` keeps the placeholder up; no empty `<img>` is rendered.
|
|
60
|
+
|
|
61
|
+
**Give it a width.** With no `width` the placeholder shows at its own size,
|
|
62
|
+
which for a low-res placeholder is usually tiny. Set one, or `width="100%"`.
|
|
63
|
+
|
|
46
64
|
### With Error Fallback
|
|
47
65
|
|
|
48
66
|
```tsx
|
|
@@ -170,8 +188,8 @@ wrapping element, rounded to match the image:
|
|
|
170
188
|
| Prop | Type | Default | Description |
|
|
171
189
|
|------|------|---------|-------------|
|
|
172
190
|
| `lazy` | `boolean` | `true` | Enable lazy loading |
|
|
173
|
-
| `loadingState` | `'skeleton' \| 'spinner' \| 'placeholder' \| 'none'` | `'skeleton'` | Type of loading indicator |
|
|
174
|
-
| `placeholder` | `string` | - |
|
|
191
|
+
| `loadingState` | `'skeleton' \| 'spinner' \| 'placeholder' \| 'none'` | `'skeleton'` | Type of loading indicator. `'placeholder'` shows the `placeholder` image until the real image has faded in over it; set a `width` (or `"100%"`) with it |
|
|
192
|
+
| `placeholder` | `string` | - | Low-res image shown while loading, only under `loadingState="placeholder"` (ignored otherwise). See [With Placeholder](#with-placeholder) |
|
|
175
193
|
| `rootMargin` | `string` | `'100px'` | Intersection Observer margin |
|
|
176
194
|
| `threshold` | `number \| number[]` | `0` | Intersection Observer threshold |
|
|
177
195
|
|
|
@@ -263,11 +281,13 @@ wrapping element, rounded to match the image:
|
|
|
263
281
|
height={48}
|
|
264
282
|
/>
|
|
265
283
|
|
|
266
|
-
// For hero images with low-res placeholders
|
|
284
|
+
// For hero images with low-res placeholders — give it a width, or the
|
|
285
|
+
// placeholder shows at its own, tiny, size
|
|
267
286
|
<LazyImage
|
|
268
287
|
src="hero-hd.jpg"
|
|
269
288
|
placeholder="hero-lowres.jpg"
|
|
270
289
|
alt="Hero image"
|
|
290
|
+
width="100%"
|
|
271
291
|
loadingState="placeholder"
|
|
272
292
|
/>
|
|
273
293
|
```
|
|
@@ -81,6 +81,13 @@ const sxLength = (theme: Theme, value: Length): string | undefined =>
|
|
|
81
81
|
const innerLength = (theme: Theme, value: Length): string | undefined =>
|
|
82
82
|
isFraction(value) ? '100%' : styleLength(theme, value);
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A width or height with its default. Only an unset or empty size takes it: `0`
|
|
86
|
+
* is a size, which `||` read as unset (FUT-2669), and `sx` reads `''` as `0%`.
|
|
87
|
+
*/
|
|
88
|
+
const orDefault = (value: Length, fallback: number | string): number | string =>
|
|
89
|
+
value === undefined || value === '' ? fallback : value;
|
|
90
|
+
|
|
84
91
|
/** The box the image occupies, as CSS, shared by the real image and every stand-in for it. */
|
|
85
92
|
interface BoxMetrics {
|
|
86
93
|
width?: string;
|
|
@@ -100,8 +107,8 @@ const SkeletonIndicator: React.FC<IndicatorProps> = ({ props }) => {
|
|
|
100
107
|
return (
|
|
101
108
|
<Skeleton
|
|
102
109
|
variant="rectangular"
|
|
103
|
-
width={innerLength(theme, props.width
|
|
104
|
-
height={innerLength(theme, props.height
|
|
110
|
+
width={innerLength(theme, orDefault(props.width, '100%'))}
|
|
111
|
+
height={innerLength(theme, orDefault(props.height, 'auto'))}
|
|
105
112
|
animation={props.skeletonProps.animation || 'pulse'}
|
|
106
113
|
intensity={props.skeletonProps.intensity}
|
|
107
114
|
borderRadius={styleLength(theme, props.borderRadius)}
|
|
@@ -126,40 +133,59 @@ const SpinnerIndicator: React.FC<IndicatorProps> = ({ props }) => {
|
|
|
126
133
|
);
|
|
127
134
|
};
|
|
128
135
|
|
|
129
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The placeholder sits in the box's flow and gives the box its size: the box's
|
|
138
|
+
* own when one is set, the placeholder's when not. The real image is drawn over
|
|
139
|
+
* it (`OVER_PLACEHOLDER`) until it has faded in. Its load and error are not the
|
|
140
|
+
* image's, so neither reaches the caller; a failing one just retires.
|
|
141
|
+
*/
|
|
142
|
+
const PlaceholderIndicator: React.FC<IndicatorProps & { onError: () => void }> = ({
|
|
130
143
|
props,
|
|
131
144
|
metrics,
|
|
132
|
-
|
|
145
|
+
onError,
|
|
133
146
|
}) => {
|
|
134
|
-
|
|
135
|
-
// would only stack behind it.
|
|
136
|
-
if (!props.placeholder || hasSrc) return null;
|
|
147
|
+
if (!props.placeholder) return null;
|
|
137
148
|
|
|
138
149
|
return (
|
|
139
150
|
<StyledImage
|
|
140
151
|
src={props.placeholder}
|
|
141
152
|
alt={`${props.alt} (loading)`}
|
|
142
153
|
style={metrics}
|
|
154
|
+
decoding={props.decoding}
|
|
155
|
+
loading={props.lazy ? 'lazy' : props.loading}
|
|
156
|
+
onError={onError}
|
|
143
157
|
data-testid={`${props['data-testid']}-placeholder`}
|
|
144
158
|
/>
|
|
145
159
|
);
|
|
146
160
|
};
|
|
147
161
|
|
|
148
162
|
const LoadingIndicator: React.FC<
|
|
149
|
-
IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>;
|
|
150
|
-
> = ({ kind,
|
|
163
|
+
IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>; onPlaceholderError: () => void }
|
|
164
|
+
> = ({ kind, onPlaceholderError, ...rest }) => {
|
|
151
165
|
switch (kind) {
|
|
152
166
|
case 'skeleton':
|
|
153
167
|
return <SkeletonIndicator {...rest} />;
|
|
154
168
|
case 'spinner':
|
|
155
169
|
return <SpinnerIndicator {...rest} />;
|
|
156
170
|
case 'placeholder':
|
|
157
|
-
return <PlaceholderIndicator {...rest}
|
|
171
|
+
return <PlaceholderIndicator {...rest} onError={onPlaceholderError} />;
|
|
158
172
|
default:
|
|
159
173
|
return null;
|
|
160
174
|
}
|
|
161
175
|
};
|
|
162
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The real image while the placeholder is up: laid over it and cropped by
|
|
179
|
+
* `objectFit`, so the box never grows to hold both. It takes the flow back
|
|
180
|
+
* when the placeholder goes.
|
|
181
|
+
*/
|
|
182
|
+
const OVER_PLACEHOLDER: React.CSSProperties = {
|
|
183
|
+
position: 'absolute',
|
|
184
|
+
inset: 0,
|
|
185
|
+
width: '100%',
|
|
186
|
+
height: '100%',
|
|
187
|
+
};
|
|
188
|
+
|
|
163
189
|
const ErrorFallback: React.FC<IndicatorProps> = ({ props, metrics }) => {
|
|
164
190
|
const theme = useTheme();
|
|
165
191
|
const { alt, fallback, width, height, borderRadius } = props;
|
|
@@ -207,8 +233,10 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
207
233
|
effectiveLoadingState,
|
|
208
234
|
handleImageLoad,
|
|
209
235
|
handleImageError,
|
|
236
|
+
handlePlaceholderError,
|
|
210
237
|
showImage,
|
|
211
238
|
showLoading,
|
|
239
|
+
showPlaceholder,
|
|
212
240
|
} = useLazyImage(props);
|
|
213
241
|
|
|
214
242
|
const { width, height, borderRadius, alt } = props;
|
|
@@ -226,8 +254,8 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
226
254
|
ref={containerRef}
|
|
227
255
|
className={props.className}
|
|
228
256
|
sx={{
|
|
229
|
-
width: sxLength(theme, width
|
|
230
|
-
height: sxLength(theme, height
|
|
257
|
+
width: sxLength(theme, orDefault(width, 'auto')),
|
|
258
|
+
height: sxLength(theme, orDefault(height, 'auto')),
|
|
231
259
|
// The same px length the image gets (`metrics`): in `sx` a bare number
|
|
232
260
|
// would be a multiple of `shape.borderRadius`, and this box clips (FUT-2656).
|
|
233
261
|
borderRadius: metrics.borderRadius,
|
|
@@ -239,7 +267,7 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
239
267
|
kind={effectiveLoadingState}
|
|
240
268
|
props={props}
|
|
241
269
|
metrics={metrics}
|
|
242
|
-
|
|
270
|
+
onPlaceholderError={handlePlaceholderError}
|
|
243
271
|
/>
|
|
244
272
|
)}
|
|
245
273
|
|
|
@@ -254,7 +282,7 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
254
282
|
isLoaded={!state.isLoading}
|
|
255
283
|
decoding={props.decoding}
|
|
256
284
|
loading={props.lazy ? 'lazy' : props.loading}
|
|
257
|
-
style={{ ...metrics, ...props.sx }}
|
|
285
|
+
style={{ ...metrics, ...props.sx, ...(showPlaceholder && OVER_PLACEHOLDER) }}
|
|
258
286
|
aria-label={props['aria-label'] || alt}
|
|
259
287
|
aria-describedby={props['aria-describedby']}
|
|
260
288
|
role={props.role}
|
|
@@ -34,7 +34,17 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
34
34
|
height?: number | string;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* A low-resolution image URL shown while the real image loads. Only
|
|
38
|
+
* `loadingState="placeholder"` draws it; under any other loading state it is
|
|
39
|
+
* ignored. It shows from mount, lazy or not, until the real image has loaded
|
|
40
|
+
* and faded in (at once when `fadeIn` is off) or has finally failed. The real
|
|
41
|
+
* image is drawn over it and cropped by `objectFit`, so the box takes the
|
|
42
|
+
* placeholder's shape until the fade ends. A placeholder that fails to load is
|
|
43
|
+
* dropped, as if none were set, and is never reported to `onError`; neither
|
|
44
|
+
* does its load reach `onLoad`.
|
|
45
|
+
*
|
|
46
|
+
* Give LazyImage a `width` (or `"100%"`) in placeholder mode: with none, the
|
|
47
|
+
* placeholder shows at its own, usually tiny, size.
|
|
38
48
|
*/
|
|
39
49
|
placeholder?: string;
|
|
40
50
|
|
|
@@ -47,8 +57,12 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
47
57
|
* Loading state type
|
|
48
58
|
* - 'skeleton': Shows a skeleton loader
|
|
49
59
|
* - 'spinner': Shows a loading spinner
|
|
50
|
-
* - 'placeholder': Shows placeholder image
|
|
60
|
+
* - 'placeholder': Shows the `placeholder` image, in the box's flow, until the
|
|
61
|
+
* real image has faded in over it; nothing when no `placeholder` is set.
|
|
62
|
+
* Set a `width` (or `"100%"`), or the placeholder shows at its own size.
|
|
51
63
|
* - 'none': No loading indicator
|
|
64
|
+
*
|
|
65
|
+
* `placeholder` applies only to 'placeholder'.
|
|
52
66
|
* @default 'skeleton'
|
|
53
67
|
*/
|
|
54
68
|
loadingState?: LazyImageLoadingState;
|
|
@@ -180,7 +194,9 @@ export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement
|
|
|
180
194
|
* Inline CSS for the `<img>` (its `style`), not MUI's `sx`: theme values and
|
|
181
195
|
* nested selectors such as `'&:hover'` do not apply. The image's box clips
|
|
182
196
|
* anything painted outside the image, such as a shadow, so put those on a
|
|
183
|
-
* wrapping element.
|
|
197
|
+
* wrapping element. While a placeholder is up (`loadingState="placeholder"`),
|
|
198
|
+
* the image is laid over it, so its `position`, `inset`, `width` and `height`
|
|
199
|
+
* are fixed until the placeholder retires; the rest applies throughout.
|
|
184
200
|
*/
|
|
185
201
|
sx?: React.CSSProperties;
|
|
186
202
|
|
|
@@ -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 retry has to be cancelled on unmount, or it sets state on a dead component. */\nconst useRetryTimeout = () => {\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 * Owns whether the real image has been requested yet and how it went: the\n * observer above, the load/error handlers, and the retry timer.\n */\nexport const useLazyImage = (props: ResolvedLazyImageProps) => {\n const { src, placeholder, lazy, loadingState, showSpinner } = props;\n const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const retryTimeoutRef = useRetryTimeout();\n\n // Handle deprecated showSpinner prop\n const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;\n\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 ? placeholder || 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 const handleImageLoad = useCallback(\n (event: React.SyntheticEvent<HTMLImageElement>) => {\n setState((prev) => ({ ...prev, isLoading: false, hasError: false, retryCount: 0 }));\n onLoad?.(event);\n },\n [onLoad],\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 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],\n );\n\n return {\n state,\n containerRef,\n effectiveLoadingState,\n handleImageLoad,\n handleImageError,\n showImage: Boolean(state.currentSrc) && !state.hasError,\n showLoading:\n state.isLoading && (!state.currentSrc || effectiveLoadingState !== 'placeholder'),\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -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/** 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, props.width || '100%')}\n height={innerLength(theme, props.height || 200)}\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\nconst PlaceholderIndicator: React.FC<IndicatorProps & { hasSrc: boolean }> = ({\n props,\n metrics,\n hasSrc,\n}) => {\n // Once a src has been chosen the real <img> is on screen, so the placeholder\n // would only stack behind it.\n if (!props.placeholder || hasSrc) return null;\n\n return (\n <StyledImage\n src={props.placeholder}\n alt={`${props.alt} (loading)`}\n style={metrics}\n data-testid={`${props['data-testid']}-placeholder`}\n />\n );\n};\n\nconst LoadingIndicator: React.FC<\n IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>; hasSrc: boolean }\n> = ({ kind, hasSrc, ...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} hasSrc={hasSrc} />;\n default:\n return null;\n }\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 showImage,\n showLoading,\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, width || 'auto'),\n height: sxLength(theme, 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 hasSrc={Boolean(state.currentSrc)}\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 }}\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"]}
|