@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.
- package/esm/_internal/Avatar2.js +1 -5
- package/esm/_internal/Avatar2.js.map +1 -1
- package/esm/_internal/DragHandle.js +1 -1
- package/esm/_internal/DragHandle.js.map +1 -1
- package/esm/_internal/Flag2.js +1 -3
- package/esm/_internal/Flag2.js.map +1 -1
- package/esm/_internal/Icon2.js +9 -1
- package/esm/_internal/Icon2.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 +2 -21
- package/esm/_internal/Slider2.js.map +1 -1
- package/esm/_internal/Thumbnail2.js +787 -81
- package/esm/_internal/Thumbnail2.js.map +1 -1
- package/esm/_internal/UserBlock.js +14 -45
- package/esm/_internal/UserBlock.js.map +1 -1
- package/esm/_internal/avatar.js +3 -0
- package/esm/_internal/avatar.js.map +1 -1
- package/esm/_internal/clamp.js +22 -0
- package/esm/_internal/clamp.js.map +1 -0
- package/esm/_internal/comment-block.js +3 -0
- package/esm/_internal/comment-block.js.map +1 -1
- package/esm/_internal/image-block.js +3 -0
- package/esm/_internal/image-block.js.map +1 -1
- package/esm/_internal/link-preview.js +3 -0
- 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 +3 -0
- package/esm/_internal/mosaic.js.map +1 -1
- package/esm/_internal/post-block.js +3 -0
- package/esm/_internal/post-block.js.map +1 -1
- package/esm/_internal/slider.js +2 -1
- package/esm/_internal/slider.js.map +1 -1
- package/esm/_internal/thumbnail.js +3 -0
- package/esm/_internal/thumbnail.js.map +1 -1
- package/esm/_internal/user-block.js +2 -1
- package/esm/_internal/user-block.js.map +1 -1
- package/esm/index.js +3 -2
- package/esm/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/avatar/Avatar.tsx +0 -8
- package/src/components/drag-handle/DragHandle.tsx +5 -1
- package/src/components/flag/Flag.test.tsx +1 -2
- package/src/components/flag/Flag.tsx +2 -10
- package/src/components/flag/__snapshots__/Flag.test.tsx.snap +0 -15
- package/src/components/icon/Icon.tsx +10 -1
- package/src/components/message/Message.tsx +2 -2
- package/src/components/thumbnail/Thumbnail.stories.tsx +42 -347
- package/src/components/thumbnail/Thumbnail.test.tsx +2 -20
- package/src/components/thumbnail/Thumbnail.tsx +45 -73
- package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +6 -53
- package/src/components/thumbnail/useFocusPoint.ts +10 -18
- package/src/components/thumbnail/useImageLoad.ts +22 -23
- package/src/components/user-block/UserBlock.stories.tsx +4 -30
- package/src/components/user-block/UserBlock.tsx +16 -41
- package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +145 -244
- package/src/hooks/useOnResize.ts +0 -6
- package/src/stories/knobs/image.ts +3 -35
- package/types.d.ts +0 -14
|
@@ -12,66 +12,30 @@ import {
|
|
|
12
12
|
Thumbnail,
|
|
13
13
|
ThumbnailVariant,
|
|
14
14
|
} from '@lumx/react';
|
|
15
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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('
|
|
74
|
-
const variant = select('
|
|
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:
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
const
|
|
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
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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={
|
|
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
|
-
</
|
|
172
|
+
</div>
|
|
201
173
|
);
|
|
202
174
|
});
|
|
203
175
|
Thumbnail.displayName = COMPONENT_NAME;
|