@lumx/react 2.1.9-alpha-thumbnail → 2.1.9

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 (61) hide show
  1. package/esm/_internal/Avatar2.js +1 -5
  2. package/esm/_internal/Avatar2.js.map +1 -1
  3. package/esm/_internal/DragHandle.js +1 -1
  4. package/esm/_internal/DragHandle.js.map +1 -1
  5. package/esm/_internal/Flag2.js +1 -3
  6. package/esm/_internal/Flag2.js.map +1 -1
  7. package/esm/_internal/Icon2.js +9 -1
  8. package/esm/_internal/Icon2.js.map +1 -1
  9. package/esm/_internal/List2.js.map +1 -1
  10. package/esm/_internal/Message2.js +2 -2
  11. package/esm/_internal/Message2.js.map +1 -1
  12. package/esm/_internal/Slider2.js +2 -21
  13. package/esm/_internal/Slider2.js.map +1 -1
  14. package/esm/_internal/Thumbnail2.js +787 -81
  15. package/esm/_internal/Thumbnail2.js.map +1 -1
  16. package/esm/_internal/UserBlock.js +14 -45
  17. package/esm/_internal/UserBlock.js.map +1 -1
  18. package/esm/_internal/avatar.js +3 -0
  19. package/esm/_internal/avatar.js.map +1 -1
  20. package/esm/_internal/clamp.js +22 -0
  21. package/esm/_internal/clamp.js.map +1 -0
  22. package/esm/_internal/comment-block.js +3 -0
  23. package/esm/_internal/comment-block.js.map +1 -1
  24. package/esm/_internal/image-block.js +3 -0
  25. package/esm/_internal/image-block.js.map +1 -1
  26. package/esm/_internal/link-preview.js +3 -0
  27. package/esm/_internal/link-preview.js.map +1 -1
  28. package/esm/_internal/mdi.js +2 -2
  29. package/esm/_internal/mdi.js.map +1 -1
  30. package/esm/_internal/mosaic.js +3 -0
  31. package/esm/_internal/mosaic.js.map +1 -1
  32. package/esm/_internal/post-block.js +3 -0
  33. package/esm/_internal/post-block.js.map +1 -1
  34. package/esm/_internal/slider.js +2 -1
  35. package/esm/_internal/slider.js.map +1 -1
  36. package/esm/_internal/thumbnail.js +3 -0
  37. package/esm/_internal/thumbnail.js.map +1 -1
  38. package/esm/_internal/user-block.js +2 -1
  39. package/esm/_internal/user-block.js.map +1 -1
  40. package/esm/index.js +3 -2
  41. package/esm/index.js.map +1 -1
  42. package/package.json +4 -4
  43. package/src/components/avatar/Avatar.tsx +0 -8
  44. package/src/components/drag-handle/DragHandle.tsx +5 -1
  45. package/src/components/flag/Flag.test.tsx +1 -2
  46. package/src/components/flag/Flag.tsx +2 -10
  47. package/src/components/flag/__snapshots__/Flag.test.tsx.snap +0 -15
  48. package/src/components/icon/Icon.tsx +10 -1
  49. package/src/components/message/Message.tsx +2 -2
  50. package/src/components/thumbnail/Thumbnail.stories.tsx +42 -347
  51. package/src/components/thumbnail/Thumbnail.test.tsx +2 -20
  52. package/src/components/thumbnail/Thumbnail.tsx +45 -73
  53. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +6 -53
  54. package/src/components/thumbnail/useFocusPoint.ts +10 -18
  55. package/src/components/thumbnail/useImageLoad.ts +22 -23
  56. package/src/components/user-block/UserBlock.stories.tsx +4 -30
  57. package/src/components/user-block/UserBlock.tsx +16 -41
  58. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +145 -244
  59. package/src/hooks/useOnResize.ts +0 -6
  60. package/src/stories/knobs/image.ts +3 -35
  61. package/types.d.ts +0 -14
@@ -12,66 +12,30 @@ import {
12
12
  Thumbnail,
13
13
  ThumbnailVariant,
14
14
  } from '@lumx/react';
15
- import { IMAGE_SIZES, imageKnob, IMAGES } from '@lumx/react/stories/knobs/image';
15
+ import { imageKnob, IMAGES } from '@lumx/react/stories/knobs/image';
16
+ import { htmlDecode } from '@lumx/react/utils/htmlDecode';
16
17
  import { boolean, select, text } from '@storybook/addon-knobs';
17
18
  import { enumKnob } from '@lumx/react/stories/knobs/enumKnob';
18
19
  import { focusKnob } from '@lumx/react/stories/knobs/focusKnob';
19
20
  import { sizeKnob } from '@lumx/react/stories/knobs/sizeKnob';
20
- import { action } from '@storybook/addon-actions';
21
- import classNames from 'classnames';
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
21
 
36
22
  export default { title: 'LumX components/thumbnail/Thumbnail' };
37
23
 
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;
24
+ export const Default = () => <Thumbnail alt="Image alt text" image={imageKnob()} size={Size.xxl} />;
54
25
 
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
- };
26
+ export const Clickable = () => <Thumbnail alt="Click me" image={imageKnob()} size={Size.xxl} onClick={console.log} />;
27
+
28
+ export const DefaultFallback = () => <Thumbnail alt="foo" image="foo" />;
29
+
30
+ export const IconFallback = () => <Thumbnail alt="foo" image="foo" fallback={mdiAbTesting} />;
31
+
32
+ export const CustomFallback = () => (
33
+ <Thumbnail alt="foo" image="foo" fallback={<Thumbnail alt="missing image" image="/logo.svg" />} />
34
+ );
71
35
 
72
36
  export const WithBadge = () => {
73
- const thumbnailSize = sizeKnob('Size', Size.l);
74
- const variant = select('Variant', ThumbnailVariant, ThumbnailVariant.rounded);
37
+ const thumbnailSize = sizeKnob('Thumbnail size', Size.l);
38
+ const variant = select('Thumbnail variant', ThumbnailVariant, ThumbnailVariant.rounded);
75
39
  const badgeColor = select('Badge color', ColorPalette, ColorPalette.primary);
76
40
  const activateFallback = boolean('Activate fallback', false);
77
41
  const image = imageKnob();
@@ -91,228 +55,13 @@ export const WithBadge = () => {
91
55
  );
92
56
  };
93
57
 
94
- export const Clickable = () => (
95
- <Thumbnail alt="Click me" image={imageKnob()} size={sizeKnob('Size', Size.xxl)} onClick={action('onClick')} />
96
- );
97
-
98
- export const ClickableLink = () => (
99
- <Thumbnail
100
- alt="Click me"
101
- image={imageKnob()}
102
- size={sizeKnob('Size', Size.xxl)}
103
- linkProps={{ href: 'https://google.fr' }}
104
- />
105
- );
106
-
107
- const CustomLinkComponent = (props: any) => (
108
- <a {...props} className={classNames('custom-link-component', props.className)}>
109
- {props.children}
110
- </a>
111
- );
112
-
113
- export const ClickableCustomLink = () => (
114
- <Thumbnail
115
- alt="Click me"
116
- image={imageKnob()}
117
- size={sizeKnob('Size', Size.xxl)}
118
- linkAs={CustomLinkComponent}
119
- linkProps={{ href: 'https://google.fr', className: 'custom-class-name' }}
120
- />
121
- );
122
-
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
- };
146
-
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
- );
212
-
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
- </>
259
- );
260
-
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
- );
308
-
309
58
  export const ParentSizeConstraint = () => {
310
59
  const fillHeight = boolean('Fill Height', true);
311
60
  return Object.values(AspectRatio).map((aspectRatio) => (
312
61
  <FlexBox key={aspectRatio} orientation="horizontal" gap="huge">
313
62
  <h1>ratio: {aspectRatio}</h1>
314
63
 
315
- <div style={{ border: '1px solid red', width: 220, height: 400, resize: 'both', overflow: 'auto' }}>
64
+ <div style={{ border: '1px solid red', width: 200, height: 400, resize: 'both', overflow: 'auto' }}>
316
65
  <Thumbnail alt="Grid" image="/demo-assets/grid.jpg" aspectRatio={aspectRatio} fillHeight={fillHeight} />
317
66
  </div>
318
67
 
@@ -327,89 +76,35 @@ export const ParentSizeConstraint = () => {
327
76
  ));
328
77
  };
329
78
 
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
- >
79
+ export const Knobs = ({ theme }: any) => {
80
+ const alt = text('Alternative text', 'Image alt text');
81
+ const align = enumKnob(
82
+ 'Alignment',
83
+ [undefined, Alignment.center, Alignment.left, Alignment.right] as const,
84
+ undefined,
85
+ );
86
+ const aspectRatio = enumKnob('Aspect ratio', [undefined, ...Object.values(AspectRatio)], undefined);
87
+ const crossOrigin = enumKnob('CORS', [undefined, 'anonymous', 'use-credentials'] as const, undefined);
88
+ const fillHeight = boolean('Fill Height', false);
89
+ const focusPoint = { x: focusKnob('Focus X'), y: focusKnob('Focus Y') };
90
+ const image = imageKnob('Image', IMAGES.landscape1);
91
+ const variant = select('Variant', ThumbnailVariant, ThumbnailVariant.squared);
92
+ const size = sizeKnob('Size', Size.xxl);
93
+ const onClick = boolean('clickable?', false) ? () => console.log('ok') : undefined;
94
+
95
+ return (
337
96
  <Thumbnail
97
+ alt={alt}
98
+ align={align}
99
+ aspectRatio={aspectRatio}
100
+ crossOrigin={crossOrigin}
101
+ fillHeight={fillHeight}
102
+ focusPoint={focusPoint}
103
+ image={htmlDecode(image)}
104
+ size={size}
338
105
  theme={theme}
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)}
106
+ variant={variant}
107
+ onClick={onClick}
344
108
  />
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
- </>
414
109
  );
415
110
  };
@@ -5,16 +5,7 @@ import 'jest-enzyme';
5
5
  import { commonTestsSuite, itShouldRenderStories } from '@lumx/react/testing/utils';
6
6
 
7
7
  import { Thumbnail, ThumbnailProps } from './Thumbnail';
8
- import {
9
- Clickable,
10
- ClickableCustomLink,
11
- ClickableLink,
12
- ErrorCustomFallback,
13
- Default,
14
- ErrorFallback,
15
- ErrorCustomIconFallback,
16
- WithBadge,
17
- } from './Thumbnail.stories';
8
+ import { Clickable, CustomFallback, Default, DefaultFallback, IconFallback, WithBadge } from './Thumbnail.stories';
18
9
 
19
10
  const CLASSNAME = Thumbnail.className as string;
20
11
 
@@ -31,16 +22,7 @@ describe(`<${Thumbnail.displayName}>`, () => {
31
22
  // 1. Test render via snapshot.
32
23
  describe('Snapshots and structure', () => {
33
24
  itShouldRenderStories(
34
- {
35
- Default,
36
- Clickable,
37
- ClickableLink,
38
- ClickableCustomLink,
39
- ErrorFallback,
40
- ErrorCustomFallback,
41
- ErrorCustomIconFallback,
42
- WithBadge,
43
- },
25
+ { Default, Clickable, DefaultFallback, CustomFallback, IconFallback, WithBadge },
44
26
  Thumbnail,
45
27
  );
46
28
  });
@@ -7,6 +7,7 @@ import React, {
7
7
  ReactNode,
8
8
  Ref,
9
9
  useRef,
10
+ useState,
10
11
  } from 'react';
11
12
  import classNames from 'classnames';
12
13
 
@@ -14,9 +15,12 @@ import { AspectRatio, HorizontalAlignment, Icon, Size, Theme } from '@lumx/react
14
15
 
15
16
  import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
16
17
 
17
- import { mdiImageBroken } from '@lumx/icons';
18
+ import { mdiImageBrokenVariant } from '@lumx/icons';
19
+ import { isInternetExplorer } from '@lumx/react/utils/isInternetExplorer';
18
20
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
21
+ import { useFocusPoint } from '@lumx/react/components/thumbnail/useFocusPoint';
19
22
  import { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';
23
+ import { useClickable } from '@lumx/react/components/thumbnail/useClickable';
20
24
  import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
21
25
 
22
26
  type ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;
@@ -47,8 +51,6 @@ export interface ThumbnailProps extends GenericProps {
47
51
  imgProps?: ImgHTMLProps;
48
52
  /** Reference to the native <img> element. */
49
53
  imgRef?: Ref<HTMLImageElement>;
50
- /** Set to true to force the display of the loading skeleton. */
51
- isLoading?: boolean;
52
54
  /** Size variant of the component. */
53
55
  size?: ThumbnailSize;
54
56
  /** Image loading mode. */
@@ -61,10 +63,6 @@ export interface ThumbnailProps extends GenericProps {
61
63
  theme?: Theme;
62
64
  /** Variant of the component. */
63
65
  variant?: ThumbnailVariant;
64
- /** Props to pass to the link wrapping the thumbnail. */
65
- linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
66
- /** Custom react component for the link (can be used to inject react router Link). */
67
- linkAs?: 'a' | any;
68
66
  }
69
67
 
70
68
  /**
@@ -81,18 +79,11 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
81
79
  * Component default props.
82
80
  */
83
81
  const DEFAULT_PROPS: Partial<ThumbnailProps> = {
84
- fallback: mdiImageBroken,
82
+ fallback: mdiImageBrokenVariant,
85
83
  loading: 'lazy',
86
84
  theme: Theme.light,
87
85
  };
88
86
 
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 `${x}% ${y}%`;
94
- }
95
-
96
87
  /**
97
88
  * Thumbnail component.
98
89
  *
@@ -104,7 +95,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
104
95
  const {
105
96
  align,
106
97
  alt,
107
- aspectRatio = AspectRatio.original,
98
+ aspectRatio,
108
99
  badge,
109
100
  className,
110
101
  crossOrigin,
@@ -114,90 +105,71 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
114
105
  image,
115
106
  imgProps,
116
107
  imgRef: propImgRef,
117
- isLoading: isLoadingProp,
118
108
  loading,
119
109
  size,
120
110
  theme,
121
111
  variant,
122
- linkProps,
123
- linkAs,
124
- showSkeletonLoading = true,
125
112
  ...forwardedProps
126
113
  } = props;
127
114
  const imgRef = useRef<HTMLImageElement>(null);
128
115
 
129
116
  // Image loading state.
130
- const loadingState = useImageLoad(image, imgRef);
131
- const isLoading = isLoadingProp || loadingState === 'isLoading';
117
+ const loadingState = useImageLoad(imgRef);
132
118
  const hasError = loadingState === 'hasError';
119
+ const isLoading = loadingState === 'isLoading';
133
120
 
134
- const isLink = Boolean(linkProps?.href || linkAs);
135
- const isButton = !!forwardedProps.onClick;
136
- const isClickable = isButton || isLink;
121
+ const [wrapper, setWrapper] = useState<HTMLElement>();
122
+ const wrapperProps: any = {
123
+ ...forwardedProps,
124
+ ref: mergeRefs(setWrapper, ref),
125
+ className: classNames(
126
+ className,
127
+ handleBasicClasses({ align, aspectRatio, prefix: CLASSNAME, size, theme, variant, hasBadge: !!badge }),
128
+ isLoading && wrapper?.getBoundingClientRect()?.height && 'lumx-color-background-dark-L6',
129
+ fillHeight && `${CLASSNAME}--fill-height`,
130
+ ),
131
+ // Handle clickable Thumbnail a11y.
132
+ ...useClickable(props),
133
+ };
137
134
 
138
- let Wrapper: any = 'div';
139
- const wrapperProps = { ...forwardedProps };
140
- if (isLink) {
141
- Wrapper = linkAs || 'a';
142
- Object.assign(wrapperProps, linkProps);
143
- } else if (isButton) {
144
- Wrapper = 'button';
145
- }
135
+ // Update img style according to focus point and aspect ratio.
136
+ const style = useFocusPoint({ image, focusPoint, aspectRatio, imgRef, loadingState, wrapper });
146
137
 
147
138
  return (
148
- <Wrapper
149
- {...wrapperProps}
150
- ref={ref}
151
- className={classNames(
152
- linkProps?.className,
153
- className,
154
- handleBasicClasses({
155
- align,
156
- aspectRatio,
157
- prefix: CLASSNAME,
158
- size,
159
- theme,
160
- variant,
161
- isClickable,
162
- hasError,
163
- isLoading: showSkeletonLoading && isLoading,
164
- hasBadge: !!badge,
165
- }),
166
- fillHeight && `${CLASSNAME}--fill-height`,
167
- )}
168
- >
169
- <div className={`${CLASSNAME}__background`}>
139
+ <div {...wrapperProps}>
140
+ <div
141
+ className={`${CLASSNAME}__background`}
142
+ style={{
143
+ ...style?.wrapper,
144
+ // Remove from layout if image not loaded correctly (use fallback)
145
+ display: hasError ? 'none' : undefined,
146
+ // Hide while loading.
147
+ visibility: isLoading ? 'hidden' : undefined,
148
+ }}
149
+ >
170
150
  <img
171
151
  {...imgProps}
172
152
  style={{
173
153
  ...imgProps?.style,
174
- //
175
- //display: hasError && (!imgProps?.width || !imgProps?.height) ? 'none' : undefined,
176
- // Hide while loading.
177
- visibility: hasError || (hasError && isLoading) ? 'hidden' : undefined,
178
- // Focus point.
179
- objectPosition: getObjectPosition(aspectRatio, focusPoint),
154
+ ...style?.image,
180
155
  }}
181
156
  ref={mergeRefs(imgRef, propImgRef)}
182
- className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}
183
- crossOrigin={crossOrigin}
157
+ className={style?.image ? `${CLASSNAME}__focused-image` : `${CLASSNAME}__image`}
158
+ crossOrigin={crossOrigin && !isInternetExplorer() ? crossOrigin : undefined}
184
159
  src={image}
185
160
  alt={alt}
186
161
  loading={loading}
187
162
  />
188
- {!isLoading && hasError && (
189
- <div className={`${CLASSNAME}__fallback`}>
190
- {typeof fallback === 'string' ? (
191
- <Icon icon={fallback} size={Size.xxs} theme={theme} />
192
- ) : (
193
- fallback
194
- )}
195
- </div>
196
- )}
197
163
  </div>
164
+ {hasError &&
165
+ (typeof fallback === 'string' ? (
166
+ <Icon className={`${CLASSNAME}__fallback`} icon={fallback} size={size || Size.m} theme={theme} />
167
+ ) : (
168
+ <div className={`${CLASSNAME}__fallback`}>{fallback}</div>
169
+ ))}
198
170
  {badge &&
199
171
  React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}
200
- </Wrapper>
172
+ </div>
201
173
  );
202
174
  });
203
175
  Thumbnail.displayName = COMPONENT_NAME;