@12-apps/ui 6.39.6 → 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-LW2LQHIM.js → chunk-BVF2BSWV.js} +20 -9
- 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 +31 -10
- package/src/components/data-display/LazyImage/LazyImage.types.ts +19 -3
- package/dist/chunk-HCT5ELST.js.map +0 -1
- package/dist/chunk-LW2LQHIM.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";
|
|
@@ -91,31 +91,40 @@ var SpinnerIndicator = ({ props }) => {
|
|
|
91
91
|
var PlaceholderIndicator = ({
|
|
92
92
|
props,
|
|
93
93
|
metrics,
|
|
94
|
-
|
|
94
|
+
onError
|
|
95
95
|
}) => {
|
|
96
|
-
if (!props.placeholder
|
|
96
|
+
if (!props.placeholder) return null;
|
|
97
97
|
return /* @__PURE__ */ jsx(
|
|
98
98
|
StyledImage,
|
|
99
99
|
{
|
|
100
100
|
src: props.placeholder,
|
|
101
101
|
alt: `${props.alt} (loading)`,
|
|
102
102
|
style: metrics,
|
|
103
|
+
decoding: props.decoding,
|
|
104
|
+
loading: props.lazy ? "lazy" : props.loading,
|
|
105
|
+
onError,
|
|
103
106
|
"data-testid": `${props["data-testid"]}-placeholder`
|
|
104
107
|
}
|
|
105
108
|
);
|
|
106
109
|
};
|
|
107
|
-
var LoadingIndicator = ({ kind,
|
|
110
|
+
var LoadingIndicator = ({ kind, onPlaceholderError, ...rest }) => {
|
|
108
111
|
switch (kind) {
|
|
109
112
|
case "skeleton":
|
|
110
113
|
return /* @__PURE__ */ jsx(SkeletonIndicator, { ...rest });
|
|
111
114
|
case "spinner":
|
|
112
115
|
return /* @__PURE__ */ jsx(SpinnerIndicator, { ...rest });
|
|
113
116
|
case "placeholder":
|
|
114
|
-
return /* @__PURE__ */ jsx(PlaceholderIndicator, { ...rest,
|
|
117
|
+
return /* @__PURE__ */ jsx(PlaceholderIndicator, { ...rest, onError: onPlaceholderError });
|
|
115
118
|
default:
|
|
116
119
|
return null;
|
|
117
120
|
}
|
|
118
121
|
};
|
|
122
|
+
var OVER_PLACEHOLDER = {
|
|
123
|
+
position: "absolute",
|
|
124
|
+
inset: 0,
|
|
125
|
+
width: "100%",
|
|
126
|
+
height: "100%"
|
|
127
|
+
};
|
|
119
128
|
var ErrorFallback = ({ props, metrics }) => {
|
|
120
129
|
const theme = useTheme();
|
|
121
130
|
const { alt, fallback, width, height, borderRadius } = props;
|
|
@@ -156,8 +165,10 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
156
165
|
effectiveLoadingState,
|
|
157
166
|
handleImageLoad,
|
|
158
167
|
handleImageError,
|
|
168
|
+
handlePlaceholderError,
|
|
159
169
|
showImage,
|
|
160
|
-
showLoading
|
|
170
|
+
showLoading,
|
|
171
|
+
showPlaceholder
|
|
161
172
|
} = useLazyImage(props);
|
|
162
173
|
const { width, height, borderRadius, alt } = props;
|
|
163
174
|
const testId = props["data-testid"];
|
|
@@ -188,7 +199,7 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
188
199
|
kind: effectiveLoadingState,
|
|
189
200
|
props,
|
|
190
201
|
metrics,
|
|
191
|
-
|
|
202
|
+
onPlaceholderError: handlePlaceholderError
|
|
192
203
|
}
|
|
193
204
|
),
|
|
194
205
|
showImage && /* @__PURE__ */ jsx(
|
|
@@ -203,7 +214,7 @@ var LazyImage = React.memo(function LazyImage2(rawProps) {
|
|
|
203
214
|
isLoaded: !state.isLoading,
|
|
204
215
|
decoding: props.decoding,
|
|
205
216
|
loading: props.lazy ? "lazy" : props.loading,
|
|
206
|
-
style: { ...metrics, ...props.sx },
|
|
217
|
+
style: { ...metrics, ...props.sx, ...showPlaceholder && OVER_PLACEHOLDER },
|
|
207
218
|
"aria-label": props["aria-label"] || alt,
|
|
208
219
|
"aria-describedby": props["aria-describedby"],
|
|
209
220
|
role: props.role,
|
|
@@ -221,4 +232,4 @@ LazyImage.displayName = "LazyImage";
|
|
|
221
232
|
export {
|
|
222
233
|
LazyImage
|
|
223
234
|
};
|
|
224
|
-
//# 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
|
```
|
|
@@ -133,40 +133,59 @@ const SpinnerIndicator: React.FC<IndicatorProps> = ({ props }) => {
|
|
|
133
133
|
);
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
-
|
|
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 }> = ({
|
|
137
143
|
props,
|
|
138
144
|
metrics,
|
|
139
|
-
|
|
145
|
+
onError,
|
|
140
146
|
}) => {
|
|
141
|
-
|
|
142
|
-
// would only stack behind it.
|
|
143
|
-
if (!props.placeholder || hasSrc) return null;
|
|
147
|
+
if (!props.placeholder) return null;
|
|
144
148
|
|
|
145
149
|
return (
|
|
146
150
|
<StyledImage
|
|
147
151
|
src={props.placeholder}
|
|
148
152
|
alt={`${props.alt} (loading)`}
|
|
149
153
|
style={metrics}
|
|
154
|
+
decoding={props.decoding}
|
|
155
|
+
loading={props.lazy ? 'lazy' : props.loading}
|
|
156
|
+
onError={onError}
|
|
150
157
|
data-testid={`${props['data-testid']}-placeholder`}
|
|
151
158
|
/>
|
|
152
159
|
);
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
const LoadingIndicator: React.FC<
|
|
156
|
-
IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>;
|
|
157
|
-
> = ({ kind,
|
|
163
|
+
IndicatorProps & { kind: NonNullable<LazyImageProps['loadingState']>; onPlaceholderError: () => void }
|
|
164
|
+
> = ({ kind, onPlaceholderError, ...rest }) => {
|
|
158
165
|
switch (kind) {
|
|
159
166
|
case 'skeleton':
|
|
160
167
|
return <SkeletonIndicator {...rest} />;
|
|
161
168
|
case 'spinner':
|
|
162
169
|
return <SpinnerIndicator {...rest} />;
|
|
163
170
|
case 'placeholder':
|
|
164
|
-
return <PlaceholderIndicator {...rest}
|
|
171
|
+
return <PlaceholderIndicator {...rest} onError={onPlaceholderError} />;
|
|
165
172
|
default:
|
|
166
173
|
return null;
|
|
167
174
|
}
|
|
168
175
|
};
|
|
169
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
|
+
|
|
170
189
|
const ErrorFallback: React.FC<IndicatorProps> = ({ props, metrics }) => {
|
|
171
190
|
const theme = useTheme();
|
|
172
191
|
const { alt, fallback, width, height, borderRadius } = props;
|
|
@@ -214,8 +233,10 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
214
233
|
effectiveLoadingState,
|
|
215
234
|
handleImageLoad,
|
|
216
235
|
handleImageError,
|
|
236
|
+
handlePlaceholderError,
|
|
217
237
|
showImage,
|
|
218
238
|
showLoading,
|
|
239
|
+
showPlaceholder,
|
|
219
240
|
} = useLazyImage(props);
|
|
220
241
|
|
|
221
242
|
const { width, height, borderRadius, alt } = props;
|
|
@@ -246,7 +267,7 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
246
267
|
kind={effectiveLoadingState}
|
|
247
268
|
props={props}
|
|
248
269
|
metrics={metrics}
|
|
249
|
-
|
|
270
|
+
onPlaceholderError={handlePlaceholderError}
|
|
250
271
|
/>
|
|
251
272
|
)}
|
|
252
273
|
|
|
@@ -261,7 +282,7 @@ export const LazyImage = React.memo<LazyImageProps>(function LazyImage(rawProps)
|
|
|
261
282
|
isLoaded={!state.isLoading}
|
|
262
283
|
decoding={props.decoding}
|
|
263
284
|
loading={props.lazy ? 'lazy' : props.loading}
|
|
264
|
-
style={{ ...metrics, ...props.sx }}
|
|
285
|
+
style={{ ...metrics, ...props.sx, ...(showPlaceholder && OVER_PLACEHOLDER) }}
|
|
265
286
|
aria-label={props['aria-label'] || alt}
|
|
266
287
|
aria-describedby={props['aria-describedby']}
|
|
267
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/**\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\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, 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 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"]}
|