@lumx/react 2.1.7 → 2.1.9-alpha-thumbnail2

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.
Files changed (49) hide show
  1. package/esm/_internal/DragHandle.js +1 -1
  2. package/esm/_internal/DragHandle.js.map +1 -1
  3. package/esm/_internal/Flag2.js +3 -1
  4. package/esm/_internal/Flag2.js.map +1 -1
  5. package/esm/_internal/List2.js.map +1 -1
  6. package/esm/_internal/Message2.js +2 -2
  7. package/esm/_internal/Message2.js.map +1 -1
  8. package/esm/_internal/Slider2.js +21 -2
  9. package/esm/_internal/Slider2.js.map +1 -1
  10. package/esm/_internal/Thumbnail2.js +61 -764
  11. package/esm/_internal/Thumbnail2.js.map +1 -1
  12. package/esm/_internal/avatar.js +0 -3
  13. package/esm/_internal/avatar.js.map +1 -1
  14. package/esm/_internal/comment-block.js +0 -3
  15. package/esm/_internal/comment-block.js.map +1 -1
  16. package/esm/_internal/image-block.js +0 -3
  17. package/esm/_internal/image-block.js.map +1 -1
  18. package/esm/_internal/link-preview.js +0 -3
  19. package/esm/_internal/link-preview.js.map +1 -1
  20. package/esm/_internal/mdi.js +2 -2
  21. package/esm/_internal/mdi.js.map +1 -1
  22. package/esm/_internal/mosaic.js +0 -3
  23. package/esm/_internal/mosaic.js.map +1 -1
  24. package/esm/_internal/post-block.js +0 -3
  25. package/esm/_internal/post-block.js.map +1 -1
  26. package/esm/_internal/slider.js +1 -2
  27. package/esm/_internal/slider.js.map +1 -1
  28. package/esm/_internal/thumbnail.js +0 -3
  29. package/esm/_internal/thumbnail.js.map +1 -1
  30. package/esm/_internal/user-block.js +0 -2
  31. package/esm/_internal/user-block.js.map +1 -1
  32. package/esm/index.js +2 -3
  33. package/esm/index.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/components/drag-handle/DragHandle.tsx +1 -5
  36. package/src/components/flag/Flag.test.tsx +2 -1
  37. package/src/components/flag/Flag.tsx +10 -2
  38. package/src/components/flag/__snapshots__/Flag.test.tsx.snap +15 -0
  39. package/src/components/message/Message.tsx +2 -2
  40. package/src/components/thumbnail/Thumbnail.stories.tsx +343 -59
  41. package/src/components/thumbnail/Thumbnail.test.tsx +6 -6
  42. package/src/components/thumbnail/Thumbnail.tsx +35 -34
  43. package/src/components/thumbnail/useFocusPoint.ts +18 -10
  44. package/src/components/thumbnail/useImageLoad.ts +23 -22
  45. package/src/hooks/useOnResize.ts +6 -0
  46. package/src/stories/knobs/image.ts +35 -3
  47. package/types.d.ts +2 -0
  48. package/esm/_internal/clamp.js +0 -22
  49. package/esm/_internal/clamp.js.map +0 -1
@@ -12,22 +12,96 @@ import {
12
12
  Thumbnail,
13
13
  ThumbnailVariant,
14
14
  } from '@lumx/react';
15
- import { imageKnob, IMAGES } from '@lumx/react/stories/knobs/image';
16
- import { htmlDecode } from '@lumx/react/utils/htmlDecode';
15
+ import { IMAGE_SIZES, imageKnob, IMAGES } from '@lumx/react/stories/knobs/image';
17
16
  import { boolean, select, text } from '@storybook/addon-knobs';
18
17
  import { enumKnob } from '@lumx/react/stories/knobs/enumKnob';
19
18
  import { focusKnob } from '@lumx/react/stories/knobs/focusKnob';
20
19
  import { sizeKnob } from '@lumx/react/stories/knobs/sizeKnob';
20
+ import { action } from '@storybook/addon-actions';
21
21
  import classNames from 'classnames';
22
22
 
23
+ const knobAspectRatio = () => {
24
+ const ratiosProps = {
25
+ Original: {},
26
+ 'Original (with natural size)': { imgNaturalSize: { width: 800, height: 600 } },
27
+ 'Free (with fill height)': { aspectRatio: AspectRatio.free },
28
+ Horizontal: { aspectRatio: AspectRatio.horizontal },
29
+ Wide: { aspectRatio: AspectRatio.wide },
30
+ Vertical: { aspectRatio: AspectRatio.vertical },
31
+ Square: { aspectRatio: AspectRatio.square },
32
+ } as const;
33
+ return select('Aspect ratio', ratiosProps as any, ratiosProps.Original as any);
34
+ };
35
+
23
36
  export default { title: 'LumX components/thumbnail/Thumbnail' };
24
37
 
25
- export const Default = () => <Thumbnail alt="Image alt text" image={imageKnob()} size={Size.xxl} />;
38
+ /** Default thumbnail props (editable via knobs) */
39
+ export const Default = ({ theme }: any) => {
40
+ const alt = text('Alternative text', 'Image alt text');
41
+ const align = enumKnob(
42
+ 'Alignment',
43
+ [undefined, Alignment.center, Alignment.left, Alignment.right] as const,
44
+ undefined,
45
+ );
46
+ const aspectRatio = enumKnob('Aspect ratio', [undefined, ...Object.values(AspectRatio)], undefined);
47
+ const crossOrigin = enumKnob('CORS', [undefined, 'anonymous', 'use-credentials'] as const, undefined);
48
+ const fillHeight = boolean('Fill Height', false);
49
+ const focusPoint = { x: focusKnob('Focus X'), y: focusKnob('Focus Y') };
50
+ const image = imageKnob('Image', IMAGES.landscape1);
51
+ const variant = select('Variant', ThumbnailVariant, ThumbnailVariant.squared);
52
+ const size = sizeKnob('Size', undefined);
53
+ const onClick = boolean('clickable?', false) ? action('onClick') : undefined;
26
54
 
27
- export const Clickable = () => <Thumbnail alt="Click me" image={imageKnob()} size={Size.xxl} onClick={console.log} />;
55
+ return (
56
+ <Thumbnail
57
+ alt={alt}
58
+ align={align}
59
+ aspectRatio={aspectRatio}
60
+ crossOrigin={crossOrigin}
61
+ fillHeight={fillHeight}
62
+ focusPoint={focusPoint}
63
+ image={image}
64
+ size={size}
65
+ theme={theme}
66
+ variant={variant}
67
+ onClick={onClick}
68
+ />
69
+ );
70
+ };
71
+
72
+ export const WithBadge = () => {
73
+ const thumbnailSize = sizeKnob('Size', Size.l);
74
+ const variant = select('Variant', ThumbnailVariant, ThumbnailVariant.rounded);
75
+ const badgeColor = select('Badge color', ColorPalette, ColorPalette.primary);
76
+ const activateFallback = boolean('Activate fallback', false);
77
+ const image = imageKnob();
78
+ return (
79
+ <Thumbnail
80
+ alt="Image alt text"
81
+ image={activateFallback ? '' : image}
82
+ variant={variant}
83
+ aspectRatio={AspectRatio.square}
84
+ size={thumbnailSize}
85
+ badge={
86
+ <Badge color={badgeColor}>
87
+ <Icon icon={mdiAbTesting} />
88
+ </Badge>
89
+ }
90
+ />
91
+ );
92
+ };
93
+
94
+ export const Clickable = () => (
95
+ <Thumbnail alt="Click me" image={imageKnob()} size={sizeKnob('Size', Size.xxl)} onClick={action('onClick')} />
96
+ );
28
97
 
29
98
  export const ClickableLink = () => (
30
- <Thumbnail alt="Click me" image={imageKnob()} size={Size.xxl} linkProps={{ href: 'https://google.fr' }} />
99
+ <Thumbnail
100
+ alt="Click me"
101
+ image={imageKnob()}
102
+ size={sizeKnob('Size', Size.xxl)}
103
+ linkProps={{ href: 'https://google.fr' }}
104
+ />
31
105
  );
32
106
 
33
107
  const CustomLinkComponent = (props: any) => (
@@ -40,41 +114,197 @@ export const ClickableCustomLink = () => (
40
114
  <Thumbnail
41
115
  alt="Click me"
42
116
  image={imageKnob()}
43
- size={Size.xxl}
117
+ size={sizeKnob('Size', Size.xxl)}
44
118
  linkAs={CustomLinkComponent}
45
119
  linkProps={{ href: 'https://google.fr', className: 'custom-class-name' }}
46
120
  />
47
121
  );
48
122
 
49
- export const DefaultFallback = () => <Thumbnail alt="foo" image="foo" />;
123
+ export const FillHeight = () => {
124
+ const parentStyle = { width: 600, height: 240, border: '1px solid red' };
125
+ return (
126
+ <>
127
+ <h2>Default</h2>
128
+ <div style={parentStyle}>
129
+ <Thumbnail alt="" image={IMAGES.landscape1s200} fillHeight />
130
+ </div>
131
+ <h2>Ratio wide</h2>
132
+ <div style={parentStyle}>
133
+ <Thumbnail alt="" image={IMAGES.landscape1s200} fillHeight aspectRatio="wide" />
134
+ </div>
135
+ <h2>Ratio vertical</h2>
136
+ <div style={parentStyle}>
137
+ <Thumbnail alt="" image={IMAGES.landscape1s200} fillHeight aspectRatio="vertical" />
138
+ </div>
139
+ <h2>Ratio free</h2>
140
+ <div style={parentStyle}>
141
+ <Thumbnail alt="" image={IMAGES.landscape1s200} fillHeight aspectRatio="free" />
142
+ </div>
143
+ </>
144
+ );
145
+ };
50
146
 
51
- export const IconFallback = () => <Thumbnail alt="foo" image="foo" fallback={mdiAbTesting} />;
147
+ export const Original = () => (
148
+ <>
149
+ <h1>Ratio: Original</h1>
150
+ <h2>Default</h2>
151
+ <table>
152
+ <tr>
153
+ <th>Landscape</th>
154
+ <th>
155
+ Landscape <small>(with original size)</small>
156
+ </th>
157
+ <th>Portrait</th>
158
+ <th>
159
+ Portrait <small>(with original size)</small>
160
+ </th>
161
+ </tr>
162
+ <tr>
163
+ <td>
164
+ <Thumbnail alt="" image={IMAGES.landscape1} />
165
+ </td>
166
+ <td>
167
+ <Thumbnail alt="" image={IMAGES.landscape1} imgProps={IMAGE_SIZES.landscape1} />
168
+ </td>
169
+ <td>
170
+ <Thumbnail alt="" image={IMAGES.portrait1} />
171
+ </td>
172
+ <td>
173
+ <Thumbnail alt="" image={IMAGES.portrait1} imgProps={IMAGE_SIZES.portrait1} />
174
+ </td>
175
+ </tr>
176
+ </table>
177
+ <h2>Constrained parent size</h2>
178
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
179
+ <div className="parent" style={{ width: 220 }}>
180
+ <Thumbnail alt="" image={IMAGES.landscape1} />
181
+ </div>
182
+ <div className="parent" style={{ width: 220 }}>
183
+ <Thumbnail alt="" image={IMAGES.portrait1} />
184
+ </div>
185
+ </FlexBox>
186
+ <h2>With size</h2>
187
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
188
+ <Thumbnail alt="" image={IMAGES.landscape1} size="xxl" />
189
+ <Thumbnail alt="" image={IMAGES.portrait1} size="xxl" />
190
+ </FlexBox>
191
+ <h2>With size & smaller image</h2>
192
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
193
+ <Thumbnail alt="" image={IMAGES.landscape1s200} size="xxl" />
194
+ <Thumbnail alt="" image={IMAGES.portrait1s200} size="xxl" />
195
+ </FlexBox>
196
+ <h2>With size & smaller image & fill height</h2>
197
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
198
+ <Thumbnail alt="" image={IMAGES.landscape1s200} size="xxl" fillHeight />
199
+ <Thumbnail alt="" image={IMAGES.portrait1s200} size="xxl" fillHeight />
200
+ </FlexBox>
201
+ <h2>Constrained parent size & smaller image & fill height</h2>
202
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
203
+ <div className="parent" style={{ width: 220 }}>
204
+ <Thumbnail alt="" image={IMAGES.landscape1s200} fillHeight />
205
+ </div>
206
+ <div className="parent" style={{ width: 220 }}>
207
+ <Thumbnail alt="" image={IMAGES.portrait1s200} fillHeight />
208
+ </div>
209
+ </FlexBox>
210
+ </>
211
+ );
52
212
 
53
- export const CustomFallback = () => (
54
- <Thumbnail alt="foo" image="foo" fallback={<Thumbnail alt="missing image" image="/logo.svg" />} />
213
+ export const Vertical = () => (
214
+ <>
215
+ <h1>Ratio: vertical</h1>
216
+ <h2>Constraint parent size</h2>
217
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
218
+ <div className="parent" style={{ width: 220 }}>
219
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1} />
220
+ </div>
221
+ <div className="parent" style={{ width: 220 }}>
222
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1} />
223
+ </div>
224
+ </FlexBox>
225
+ <h2>Constraint parent size & smaller image</h2>
226
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
227
+ <div className="parent" style={{ width: 220 }}>
228
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} />
229
+ </div>
230
+ <div className="parent" style={{ width: 220 }}>
231
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} />
232
+ </div>
233
+ </FlexBox>
234
+ <h2>With size</h2>
235
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
236
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1} size="xxl" />
237
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1} size="xxl" />
238
+ </FlexBox>
239
+ <h2>With size & smaller image</h2>
240
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
241
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} size="xxl" />
242
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} size="xxl" />
243
+ </FlexBox>
244
+ <h2>With size & smaller image & fill height</h2>
245
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
246
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} size="xxl" fillHeight />
247
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} size="xxl" fillHeight />
248
+ </FlexBox>
249
+ <h2>Constrained parent size & smaller image & fill height</h2>
250
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
251
+ <div className="parent" style={{ width: 220 }}>
252
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} fillHeight />
253
+ </div>
254
+ <div className="parent" style={{ width: 220 }}>
255
+ <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} fillHeight />
256
+ </div>
257
+ </FlexBox>
258
+ </>
55
259
  );
56
260
 
57
- export const WithBadge = () => {
58
- const thumbnailSize = sizeKnob('Thumbnail size', Size.l);
59
- const variant = select('Thumbnail variant', ThumbnailVariant, ThumbnailVariant.rounded);
60
- const badgeColor = select('Badge color', ColorPalette, ColorPalette.primary);
61
- const activateFallback = boolean('Activate fallback', false);
62
- const image = imageKnob();
63
- return (
64
- <Thumbnail
65
- alt="Image alt text"
66
- image={activateFallback ? '' : image}
67
- variant={variant}
68
- aspectRatio={AspectRatio.square}
69
- size={thumbnailSize}
70
- badge={
71
- <Badge color={badgeColor}>
72
- <Icon icon={mdiAbTesting} />
73
- </Badge>
74
- }
75
- />
76
- );
77
- };
261
+ export const Wide = () => (
262
+ <>
263
+ <h1>Ratio: wide</h1>
264
+ <h2>Constrained parent size</h2>
265
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
266
+ <div className="parent" style={{ width: 220 }}>
267
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1} />
268
+ </div>
269
+ <div className="parent" style={{ width: 220 }}>
270
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1} />
271
+ </div>
272
+ </FlexBox>
273
+ <h2>Constrained parent size & smaller image</h2>
274
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
275
+ <div className="parent" style={{ width: 220 }}>
276
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} />
277
+ </div>
278
+ <div className="parent" style={{ width: 220 }}>
279
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} />
280
+ </div>
281
+ </FlexBox>
282
+ <h2>With size</h2>
283
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
284
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1} size="xxl" />
285
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1} size="xxl" />
286
+ </FlexBox>
287
+ <h2>With size & smaller image</h2>
288
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
289
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} size="xxl" />
290
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} size="xxl" />
291
+ </FlexBox>
292
+ <h2>With size & smaller image & fill height</h2>
293
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
294
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} size="xxl" fillHeight />
295
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} size="xxl" fillHeight />
296
+ </FlexBox>
297
+ <h2>Constrained parent size & smaller image & fill height</h2>
298
+ <FlexBox orientation="horizontal" vAlign="center" gap="huge">
299
+ <div className="parent" style={{ width: 220 }}>
300
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} fillHeight />
301
+ </div>
302
+ <div className="parent" style={{ width: 220 }}>
303
+ <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} fillHeight />
304
+ </div>
305
+ </FlexBox>
306
+ </>
307
+ );
78
308
 
79
309
  export const ParentSizeConstraint = () => {
80
310
  const fillHeight = boolean('Fill Height', true);
@@ -82,7 +312,7 @@ export const ParentSizeConstraint = () => {
82
312
  <FlexBox key={aspectRatio} orientation="horizontal" gap="huge">
83
313
  <h1>ratio: {aspectRatio}</h1>
84
314
 
85
- <div style={{ border: '1px solid red', width: 200, height: 400, resize: 'both', overflow: 'auto' }}>
315
+ <div style={{ border: '1px solid red', width: 220, height: 400, resize: 'both', overflow: 'auto' }}>
86
316
  <Thumbnail alt="Grid" image="/demo-assets/grid.jpg" aspectRatio={aspectRatio} fillHeight={fillHeight} />
87
317
  </div>
88
318
 
@@ -97,35 +327,89 @@ export const ParentSizeConstraint = () => {
97
327
  ));
98
328
  };
99
329
 
100
- export const Knobs = ({ theme }: any) => {
101
- const alt = text('Alternative text', 'Image alt text');
102
- const align = enumKnob(
103
- 'Alignment',
104
- [undefined, Alignment.center, Alignment.left, Alignment.right] as const,
105
- undefined,
106
- );
107
- const aspectRatio = enumKnob('Aspect ratio', [undefined, ...Object.values(AspectRatio)], undefined);
108
- const crossOrigin = enumKnob('CORS', [undefined, 'anonymous', 'use-credentials'] as const, undefined);
109
- const fillHeight = boolean('Fill Height', false);
110
- const focusPoint = { x: focusKnob('Focus X'), y: focusKnob('Focus Y') };
111
- const image = imageKnob('Image', IMAGES.landscape1);
112
- const variant = select('Variant', ThumbnailVariant, ThumbnailVariant.squared);
113
- const size = sizeKnob('Size', Size.xxl);
114
- const onClick = boolean('clickable?', false) ? () => console.log('ok') : undefined;
115
-
116
- return (
330
+ export const IsLoading = ({ theme }: any) => (
331
+ <FlexBox
332
+ orientation="horizontal"
333
+ vAlign="center"
334
+ marginAuto={['left', 'right']}
335
+ style={{ border: '1px solid red', width: 900, height: 700, resize: 'both', overflow: 'auto' }}
336
+ >
117
337
  <Thumbnail
118
- alt={alt}
119
- align={align}
120
- aspectRatio={aspectRatio}
121
- crossOrigin={crossOrigin}
122
- fillHeight={fillHeight}
123
- focusPoint={focusPoint}
124
- image={htmlDecode(image)}
125
- size={size}
126
338
  theme={theme}
127
- variant={variant}
128
- onClick={onClick}
339
+ alt="Image alt text"
340
+ image={IMAGES.landscape2}
341
+ isLoading={boolean('Is loading', true)}
342
+ fillHeight={boolean('Fill Height', false)}
343
+ size={sizeKnob('Size', undefined)}
129
344
  />
345
+ </FlexBox>
346
+ );
347
+
348
+ export const ErrorFallback = () => <Thumbnail alt="foo" image="foo" />;
349
+
350
+ export const ErrorCustomIconFallback = () => <Thumbnail alt="foo" image="foo" fallback={mdiAbTesting} />;
351
+
352
+ export const ErrorCustomFallback = () => (
353
+ <Thumbnail alt="foo" image="foo" fallback={<Thumbnail alt="missing image" image="/logo.svg" />} />
354
+ );
355
+
356
+ export const ErrorFallbackVariants = () => {
357
+ const isLoading = boolean('is loading', false);
358
+ const variant = select('Variant', ThumbnailVariant, undefined);
359
+ const base = { alt: 'foo', image: 'foo', isLoading, variant } as const;
360
+ const imageFallback = <img src="/logo.svg" alt="logo" />;
361
+ const imgProps = { width: 50, height: 50 };
362
+ return (
363
+ <>
364
+ <h2>Default</h2>
365
+ Default fallback | Custom icon fallback | Custom react node fallback
366
+ <FlexBox orientation="horizontal" gap="big">
367
+ <Thumbnail {...base} />
368
+ <Thumbnail {...base} fallback={mdiAbTesting} />
369
+ <Thumbnail {...base} fallback={imageFallback} />
370
+ </FlexBox>
371
+ <h2>
372
+ With original image size <small>(50x50)</small>
373
+ </h2>
374
+ <FlexBox orientation="horizontal" gap="big">
375
+ <Thumbnail {...base} imgProps={imgProps} />
376
+ <Thumbnail {...base} fallback={mdiAbTesting} imgProps={imgProps} />
377
+ <Thumbnail {...base} fallback={imageFallback} imgProps={imgProps} />
378
+ </FlexBox>
379
+ <h2>With size</h2>
380
+ <FlexBox orientation="horizontal" gap="big">
381
+ <Thumbnail {...base} size="xl" />
382
+ <Thumbnail {...base} size="xl" fallback={mdiAbTesting} />
383
+ <Thumbnail {...base} size="xl" fallback={imageFallback} imgProps={imgProps} />
384
+ </FlexBox>
385
+ <h2>With size & ratio</h2>
386
+ <FlexBox orientation="horizontal" gap="big">
387
+ <Thumbnail {...base} size="xl" aspectRatio="wide" />
388
+ <Thumbnail {...base} size="xl" aspectRatio="wide" fallback={mdiAbTesting} />
389
+ <Thumbnail {...base} size="xl" aspectRatio="wide" fallback={imageFallback} />
390
+ </FlexBox>
391
+ <h2>
392
+ With original size <small>(50x50)</small> & ratio
393
+ </h2>
394
+ <FlexBox orientation="horizontal" gap="big">
395
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" />
396
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" fallback={mdiAbTesting} />
397
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" fallback={imageFallback} />
398
+ </FlexBox>
399
+ <h2>
400
+ With original size <small>(50x50)</small> & ratio & constrained parent size
401
+ </h2>
402
+ <FlexBox orientation="horizontal" gap="big">
403
+ <div className="parent" style={{ width: 220 }}>
404
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" />
405
+ </div>
406
+ <div className="parent" style={{ width: 220 }}>
407
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" fallback={mdiAbTesting} />
408
+ </div>
409
+ <div className="parent" style={{ width: 220 }}>
410
+ <Thumbnail {...base} imgProps={imgProps} aspectRatio="wide" fallback={imageFallback} />
411
+ </div>
412
+ </FlexBox>
413
+ </>
130
414
  );
131
415
  };
@@ -9,10 +9,10 @@ import {
9
9
  Clickable,
10
10
  ClickableCustomLink,
11
11
  ClickableLink,
12
- CustomFallback,
12
+ ErrorCustomFallback,
13
13
  Default,
14
- DefaultFallback,
15
- IconFallback,
14
+ ErrorFallback,
15
+ ErrorCustomIconFallback,
16
16
  WithBadge,
17
17
  } from './Thumbnail.stories';
18
18
 
@@ -36,9 +36,9 @@ describe(`<${Thumbnail.displayName}>`, () => {
36
36
  Clickable,
37
37
  ClickableLink,
38
38
  ClickableCustomLink,
39
- DefaultFallback,
40
- CustomFallback,
41
- IconFallback,
39
+ ErrorFallback,
40
+ ErrorCustomFallback,
41
+ ErrorCustomIconFallback,
42
42
  WithBadge,
43
43
  },
44
44
  Thumbnail,
@@ -7,7 +7,6 @@ import React, {
7
7
  ReactNode,
8
8
  Ref,
9
9
  useRef,
10
- useState,
11
10
  } from 'react';
12
11
  import classNames from 'classnames';
13
12
 
@@ -15,10 +14,8 @@ import { AspectRatio, HorizontalAlignment, Icon, Size, Theme } from '@lumx/react
15
14
 
16
15
  import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
17
16
 
18
- import { mdiImageBrokenVariant } from '@lumx/icons';
19
- import { isInternetExplorer } from '@lumx/react/utils/isInternetExplorer';
17
+ import { mdiImageBroken } from '@lumx/icons';
20
18
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
21
- import { useFocusPoint } from '@lumx/react/components/thumbnail/useFocusPoint';
22
19
  import { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';
23
20
  import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
24
21
 
@@ -50,6 +47,8 @@ export interface ThumbnailProps extends GenericProps {
50
47
  imgProps?: ImgHTMLProps;
51
48
  /** Reference to the native <img> element. */
52
49
  imgRef?: Ref<HTMLImageElement>;
50
+ /** Set to true to force the display of the loading skeleton. */
51
+ isLoading?: boolean;
53
52
  /** Size variant of the component. */
54
53
  size?: ThumbnailSize;
55
54
  /** Image loading mode. */
@@ -82,11 +81,18 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
82
81
  * Component default props.
83
82
  */
84
83
  const DEFAULT_PROPS: Partial<ThumbnailProps> = {
85
- fallback: mdiImageBrokenVariant,
84
+ fallback: mdiImageBroken,
86
85
  loading: 'lazy',
87
86
  theme: Theme.light,
88
87
  };
89
88
 
89
+ function getObjectPosition(aspectRatio: AspectRatio, focusPoint?: FocusPoint) {
90
+ if (aspectRatio === AspectRatio.original || (!focusPoint?.y && !focusPoint?.x)) return undefined;
91
+ const x = (((focusPoint?.x || 0) + 1) / 2) * 100;
92
+ const y = (((focusPoint?.y || 0) - 1) / 2) * 100;
93
+ return `${Math.round(x)}% ${Math.round(y)}%`;
94
+ }
95
+
90
96
  /**
91
97
  * Thumbnail component.
92
98
  *
@@ -98,7 +104,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
98
104
  const {
99
105
  align,
100
106
  alt,
101
- aspectRatio,
107
+ aspectRatio = AspectRatio.original,
102
108
  badge,
103
109
  className,
104
110
  crossOrigin,
@@ -108,22 +114,22 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
108
114
  image,
109
115
  imgProps,
110
116
  imgRef: propImgRef,
117
+ isLoading: isLoadingProp,
111
118
  loading,
112
119
  size,
113
120
  theme,
114
121
  variant,
115
122
  linkProps,
116
123
  linkAs,
124
+ showSkeletonLoading = true,
117
125
  ...forwardedProps
118
126
  } = props;
119
127
  const imgRef = useRef<HTMLImageElement>(null);
120
128
 
121
129
  // Image loading state.
122
- const loadingState = useImageLoad(imgRef);
130
+ const loadingState = useImageLoad(image, imgRef);
131
+ const isLoading = isLoadingProp || loadingState === 'isLoading';
123
132
  const hasError = loadingState === 'hasError';
124
- const isLoading = loadingState === 'isLoading';
125
-
126
- const [wrapper, setWrapper] = useState<HTMLElement>();
127
133
 
128
134
  const isLink = Boolean(linkProps?.href || linkAs);
129
135
  const isButton = !!forwardedProps.onClick;
@@ -138,13 +144,10 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
138
144
  Wrapper = 'button';
139
145
  }
140
146
 
141
- // Update img style according to focus point and aspect ratio.
142
- const style = useFocusPoint({ image, focusPoint, aspectRatio, imgRef, loadingState, wrapper });
143
-
144
147
  return (
145
148
  <Wrapper
146
149
  {...wrapperProps}
147
- ref={mergeRefs(setWrapper, ref) as any}
150
+ ref={ref}
148
151
  className={classNames(
149
152
  linkProps?.className,
150
153
  className,
@@ -156,42 +159,40 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
156
159
  theme,
157
160
  variant,
158
161
  isClickable,
162
+ hasError,
163
+ isLoading: showSkeletonLoading && isLoading,
159
164
  hasBadge: !!badge,
160
165
  }),
161
- isLoading && wrapper?.getBoundingClientRect()?.height && 'lumx-color-background-dark-L6',
162
166
  fillHeight && `${CLASSNAME}--fill-height`,
163
167
  )}
164
168
  >
165
- <div
166
- className={`${CLASSNAME}__background`}
167
- style={{
168
- ...style?.wrapper,
169
- // Remove from layout if image not loaded correctly (use fallback)
170
- display: hasError ? 'none' : undefined,
171
- // Hide while loading.
172
- visibility: isLoading ? 'hidden' : undefined,
173
- }}
174
- >
169
+ <div className={`${CLASSNAME}__background`}>
175
170
  <img
176
171
  {...imgProps}
177
172
  style={{
178
173
  ...imgProps?.style,
179
- ...style?.image,
174
+ // Hide on error.
175
+ visibility: hasError ? 'hidden' : undefined,
176
+ // Focus point.
177
+ objectPosition: getObjectPosition(aspectRatio, focusPoint),
180
178
  }}
181
179
  ref={mergeRefs(imgRef, propImgRef)}
182
- className={style?.image ? `${CLASSNAME}__focused-image` : `${CLASSNAME}__image`}
183
- crossOrigin={crossOrigin && !isInternetExplorer() ? crossOrigin : undefined}
180
+ className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}
181
+ crossOrigin={crossOrigin}
184
182
  src={image}
185
183
  alt={alt}
186
184
  loading={loading}
187
185
  />
186
+ {!isLoading && hasError && (
187
+ <div className={`${CLASSNAME}__fallback`}>
188
+ {typeof fallback === 'string' ? (
189
+ <Icon icon={fallback} size={Size.xxs} theme={theme} />
190
+ ) : (
191
+ fallback
192
+ )}
193
+ </div>
194
+ )}
188
195
  </div>
189
- {hasError &&
190
- (typeof fallback === 'string' ? (
191
- <Icon className={`${CLASSNAME}__fallback`} icon={fallback} size={size || Size.m} theme={theme} />
192
- ) : (
193
- <div className={`${CLASSNAME}__fallback`}>{fallback}</div>
194
- ))}
195
196
  {badge &&
196
197
  React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}
197
198
  </Wrapper>