@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.
- package/esm/_internal/DragHandle.js +1 -1
- package/esm/_internal/DragHandle.js.map +1 -1
- package/esm/_internal/Flag2.js +3 -1
- package/esm/_internal/Flag2.js.map +1 -1
- package/esm/_internal/List2.js.map +1 -1
- package/esm/_internal/Message2.js +2 -2
- package/esm/_internal/Message2.js.map +1 -1
- package/esm/_internal/Slider2.js +21 -2
- package/esm/_internal/Slider2.js.map +1 -1
- package/esm/_internal/Thumbnail2.js +61 -764
- package/esm/_internal/Thumbnail2.js.map +1 -1
- package/esm/_internal/avatar.js +0 -3
- package/esm/_internal/avatar.js.map +1 -1
- package/esm/_internal/comment-block.js +0 -3
- package/esm/_internal/comment-block.js.map +1 -1
- package/esm/_internal/image-block.js +0 -3
- package/esm/_internal/image-block.js.map +1 -1
- package/esm/_internal/link-preview.js +0 -3
- package/esm/_internal/link-preview.js.map +1 -1
- package/esm/_internal/mdi.js +2 -2
- package/esm/_internal/mdi.js.map +1 -1
- package/esm/_internal/mosaic.js +0 -3
- package/esm/_internal/mosaic.js.map +1 -1
- package/esm/_internal/post-block.js +0 -3
- package/esm/_internal/post-block.js.map +1 -1
- package/esm/_internal/slider.js +1 -2
- package/esm/_internal/slider.js.map +1 -1
- package/esm/_internal/thumbnail.js +0 -3
- package/esm/_internal/thumbnail.js.map +1 -1
- package/esm/_internal/user-block.js +0 -2
- package/esm/_internal/user-block.js.map +1 -1
- package/esm/index.js +2 -3
- package/esm/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/drag-handle/DragHandle.tsx +1 -5
- package/src/components/flag/Flag.test.tsx +2 -1
- package/src/components/flag/Flag.tsx +10 -2
- package/src/components/flag/__snapshots__/Flag.test.tsx.snap +15 -0
- package/src/components/message/Message.tsx +2 -2
- package/src/components/thumbnail/Thumbnail.stories.tsx +343 -59
- package/src/components/thumbnail/Thumbnail.test.tsx +6 -6
- package/src/components/thumbnail/Thumbnail.tsx +35 -34
- package/src/components/thumbnail/useFocusPoint.ts +18 -10
- package/src/components/thumbnail/useImageLoad.ts +23 -22
- package/src/hooks/useOnResize.ts +6 -0
- package/src/stories/knobs/image.ts +35 -3
- package/types.d.ts +2 -0
- package/esm/_internal/clamp.js +0 -22
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
54
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
[
|
|
105
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
12
|
+
ErrorCustomFallback,
|
|
13
13
|
Default,
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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:
|
|
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={
|
|
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
|
-
|
|
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={
|
|
183
|
-
crossOrigin={crossOrigin
|
|
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>
|