@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.
@@ -2,7 +2,7 @@ import {
2
2
  imgPassThrough,
3
3
  resolveLazyImageProps,
4
4
  useLazyImage
5
- } from "../../../../chunk-HCT5ELST.js";
5
+ } from "../../../../chunk-GMUYCN33.js";
6
6
  import "../../../../chunk-YKCAJ2TN.js";
7
7
  import "../../../../chunk-PZ5AY32C.js";
8
8
  export {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LazyImage
3
- } from "../../../../chunk-SDK5LPPC.js";
4
- import "../../../../chunk-HCT5ELST.js";
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-SDK5LPPC.js";
95
- import "../../../chunk-HCT5ELST.js";
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-SDK5LPPC.js";
481
- import "../../chunk-HCT5ELST.js";
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-HCT5ELST.js";
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 || "100%"),
69
- height: innerLength(theme, props.height || 200),
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
- hasSrc
94
+ onError
94
95
  }) => {
95
- if (!props.placeholder || hasSrc) return null;
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, hasSrc, ...rest }) => {
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, hasSrc });
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 || "auto"),
177
- height: sxLength(theme, height || "auto"),
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
- hasSrc: Boolean(state.currentSrc)
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-SDK5LPPC.js.map
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":";;;;;;;;;;;;;;;;AAAA,OAAO,SAAS;AAChB,OAAO,sBAAsB;AAC7B,SAAS,QAAQ,gBAA4B;AAC7C,OAAO,WAAW;AAwGd,cAiJA,YAjJA;AAhGJ,IAAM,iBAAiB,OAAO,GAAG,EAAE,OAAO;AAAA,EACxC,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AACZ,EAAE;AAEF,IAAM,cAAc,OAAO,KAAK,EAI7B,CAAC,EAAE,QAAQ,iBAAiB,KAAK,SAAS,OAAO;AAAA,EAClD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS,SAAU,WAAW,IAAI,IAAK;AAAA,EACvC,YAAY,SAAS,WAAW,cAAc,mBAAmB;AACnE,EAAE;AAEF,IAAM,iBAAiB,OAAO,GAAG,EAAE,CAAC,EAAE,MAAM,OAAO;AAAA,EACjD,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,WAAW;AAAA,EACX,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,iBAAiB,MAAM,OAAO,GAAG;AAAA,EACjC,cAAc;AAAA,EACd,SAAS,MAAM,QAAQ,CAAC;AAC1B,EAAE;AAEF,IAAM,oBAAoB,OAAO,GAAG,EAAE,CAAC,EAAE,MAAM,OAAO;AAAA,EACpD,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,iBAAiB,MAAM,QAAQ,OAAO;AAAA,EACtC,OAAO,MAAM,QAAQ,KAAK;AAAA,EAC1B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AACR,EAAE;AAKF,IAAM,cAAc,CAAC,OAAc,UACjC,OAAO,UAAU,WAAW,IAAI,OAAO,KAAK,IAAI;AAMlD,IAAM,aAAa,CAAC,UAClB,OAAO,UAAU,YAAY,QAAQ,KAAK,SAAS;AAMrD,IAAM,WAAW,CAAC,OAAc,UAC9B,WAAW,KAAK,IAAI,GAAG,QAAQ,GAAG,MAAM,YAAY,OAAO,KAAK;AAOlE,IAAM,cAAc,CAAC,OAAc,UACjC,WAAW,KAAK,IAAI,SAAS,YAAY,OAAO,KAAK;AAMvD,IAAM,YAAY,CAAC,OAAe,aAChC,UAAU,UAAa,UAAU,KAAK,WAAW;AAgBnD,IAAM,oBAA8C,CAAC,EAAE,MAAM,MAAM;AACjE,QAAM,QAAQ,SAAS;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,OAAO,YAAY,OAAO,UAAU,MAAM,OAAO,MAAM,CAAC;AAAA,MACxD,QAAQ,YAAY,OAAO,UAAU,MAAM,QAAQ,MAAM,CAAC;AAAA,MAC1D,WAAW,MAAM,cAAc,aAAa;AAAA,MAC5C,WAAW,MAAM,cAAc;AAAA,MAC/B,cAAc,YAAY,OAAO,MAAM,YAAY;AAAA,MACnD,eAAa,GAAG,MAAM,aAAa,CAAC;AAAA;AAAA,EACtC;AAEJ;AAEA,IAAM,mBAA6C,CAAC,EAAE,MAAM,MAAM;AAChE,QAAM,QAAQ,SAAS;AACvB,QAAM,EAAE,aAAa,IAAI;AAEzB,SACE,oBAAC,kBACC;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,YAAY,OAAO,aAAa,QAAQ,EAAE;AAAA,MAChD,WAAW,aAAa,aAAa;AAAA,MACrC,IAAI,EAAE,OAAO,aAAa,SAAS,MAAM,QAAQ,QAAQ,KAAK;AAAA,MAC9D,eAAa,GAAG,MAAM,aAAa,CAAC;AAAA;AAAA,EACtC,GACF;AAEJ;AAQA,IAAM,uBAA2E,CAAC;AAAA,EAChF;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,MAAI,CAAC,MAAM,YAAa,QAAO;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,MAAM;AAAA,MACX,KAAK,GAAG,MAAM,GAAG;AAAA,MACjB,OAAO;AAAA,MACP,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM,OAAO,SAAS,MAAM;AAAA,MACrC;AAAA,MACA,eAAa,GAAG,MAAM,aAAa,CAAC;AAAA;AAAA,EACtC;AAEJ;AAEA,IAAM,mBAEF,CAAC,EAAE,MAAM,oBAAoB,GAAG,KAAK,MAAM;AAC7C,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,oBAAC,qBAAmB,GAAG,MAAM;AAAA,IACtC,KAAK;AACH,aAAO,oBAAC,oBAAkB,GAAG,MAAM;AAAA,IACrC,KAAK;AACH,aAAO,oBAAC,wBAAsB,GAAG,MAAM,SAAS,oBAAoB;AAAA,IACtE;AACE,aAAO;AAAA,EACX;AACF;AAOA,IAAM,mBAAwC;AAAA,EAC5C,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,IAAM,gBAA0C,CAAC,EAAE,OAAO,QAAQ,MAAM;AACtE,QAAM,QAAQ,SAAS;AACvB,QAAM,EAAE,KAAK,UAAU,OAAO,QAAQ,aAAa,IAAI;AACvD,QAAM,SAAS,MAAM,aAAa;AAElC,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI,OAAO,aAAa,UAAU;AAChC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK,GAAG,GAAG;AAAA,QACX,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,eAAa,GAAG,MAAM;AAAA;AAAA,IACxB;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,QACF,OAAO,YAAY,OAAO,KAAK;AAAA,QAC/B,QAAQ,YAAY,OAAO,MAAM;AAAA,QACjC,cAAc,YAAY,OAAO,YAAY;AAAA,MAC/C;AAAA,MACA,eAAa,GAAG,MAAM;AAAA,MAErB;AAAA;AAAA,EACH;AAEJ;AAMO,IAAM,YAAY,MAAM,KAAqB,SAASA,WAAU,UAAU;AAC/E,QAAM,QAAQ,sBAAsB,QAAQ;AAC5C,QAAM,QAAQ,SAAS;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,aAAa,KAAK;AAEtB,QAAM,EAAE,OAAO,QAAQ,cAAc,IAAI,IAAI;AAC7C,QAAM,SAAS,MAAM,aAAa;AAClC,QAAM,UAAsB;AAAA,IAC1B,OAAO,YAAY,OAAO,KAAK;AAAA,IAC/B,QAAQ,YAAY,OAAO,MAAM;AAAA,IACjC,WAAW,MAAM;AAAA,IACjB,gBAAgB,MAAM;AAAA,IACtB,cAAc,YAAY,OAAO,YAAY;AAAA,EAC/C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,MAAM;AAAA,MACjB,IAAI;AAAA,QACF,OAAO,SAAS,OAAO,UAAU,OAAO,MAAM,CAAC;AAAA,QAC/C,QAAQ,SAAS,OAAO,UAAU,QAAQ,MAAM,CAAC;AAAA;AAAA;AAAA,QAGjD,cAAc,QAAQ;AAAA,MACxB;AAAA,MACA,eAAa;AAAA,MAEZ;AAAA,uBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA,oBAAoB;AAAA;AAAA,QACtB;AAAA,QAGD,aACC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM,cAAc;AAAA,YACzB;AAAA,YACA,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,QAAQ,MAAM;AAAA,YACd,gBAAgB,MAAM;AAAA,YACtB,UAAU,CAAC,MAAM;AAAA,YACjB,UAAU,MAAM;AAAA,YAChB,SAAS,MAAM,OAAO,SAAS,MAAM;AAAA,YACrC,OAAO,EAAE,GAAG,SAAS,GAAG,MAAM,IAAI,GAAI,mBAAmB,iBAAkB;AAAA,YAC3E,cAAY,MAAM,YAAY,KAAK;AAAA,YACnC,oBAAkB,MAAM,kBAAkB;AAAA,YAC1C,MAAM,MAAM;AAAA,YACZ,eAAa,GAAG,MAAM;AAAA,YACrB,GAAG,eAAe,KAAK;AAAA;AAAA,QAC1B;AAAA,QAGD,MAAM,YAAY,oBAAC,iBAAc,OAAc,SAAkB;AAAA;AAAA;AAAA,EACpE;AAEJ,CAAC;AAED,UAAU,cAAc;","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 useRetryTimeout = () => {
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 useLazyImage = (props) => {
109
- const { src, placeholder, lazy, loadingState, showSpinner } = props;
110
- const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
111
- const containerRef = useRef(null);
112
- const retryTimeoutRef = useRetryTimeout();
113
- const effectiveLoadingState = showSpinner ? "spinner" : loadingState;
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 ? placeholder || null : src
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
- showLoading: state.isLoading && (!state.currentSrc || effectiveLoadingState !== "placeholder")
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-HCT5ELST.js.map
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-SDK5LPPC.js";
5
- import "../chunk-HCT5ELST.js";
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;AA6LxD;;;GAGG;AACH,eAAO,MAAM,SAAS,4CAoEpB,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, and the retry timer.
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;AAgEnF;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,sBAAsB;;;;6BAkC9C,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;8BAQtC,KAAK,CAAC,cAAc,CAAC,gBAAgB,CAAC;;;CA4BjD,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
- * Placeholder image URL to show while loading
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "6.39.5",
3
+ "version": "6.39.7",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@10.34.5",
6
6
  "type": "module",
@@ -115,8 +115,8 @@ const useVisibility = (
115
115
  }, [lazy, src, rootMargin, threshold, isVisible, onLoadStart, onVisible, containerRef]);
116
116
  };
117
117
 
118
- /** A pending retry has to be cancelled on unmount, or it sets state on a dead component. */
119
- const useRetryTimeout = () => {
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
- * Owns whether the real image has been requested yet and how it went: the
136
- * observer above, the load/error handlers, and the retry timer.
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
- export const useLazyImage = (props: ResolvedLazyImageProps) => {
139
- const { src, placeholder, lazy, loadingState, showSpinner } = props;
140
- const { onLoad, onError, retryOnError, maxRetries, retryDelay } = props;
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 containerRef = useRef<HTMLDivElement>(null);
143
- const retryTimeoutRef = useRetryTimeout();
152
+ const retire = useCallback(() => setRetired(true), []);
144
153
 
145
- // Handle deprecated showSpinner prop
146
- const effectiveLoadingState = showSpinner ? 'spinner' : loadingState;
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 ? placeholder || null : src,
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
- showLoading:
206
- state.isLoading && (!state.currentSrc || effectiveLoadingState !== 'placeholder'),
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` | - | Placeholder image URL |
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 || '100%')}
104
- height={innerLength(theme, props.height || 200)}
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
- const PlaceholderIndicator: React.FC<IndicatorProps & { hasSrc: boolean }> = ({
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
- hasSrc,
145
+ onError,
133
146
  }) => {
134
- // Once a src has been chosen the real <img> is on screen, so the placeholder
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']>; hasSrc: boolean }
150
- > = ({ kind, hasSrc, ...rest }) => {
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} hasSrc={hasSrc} />;
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 || 'auto'),
230
- height: sxLength(theme, height || 'auto'),
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
- hasSrc={Boolean(state.currentSrc)}
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
- * Placeholder image URL to show while loading
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"]}