@lumx/react 3.7.6-alpha.11 → 3.7.6-alpha.13
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/index.d.ts +1 -1
- package/index.js +29 -31
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/image-block/ImageBlock.stories.tsx +74 -63
- package/src/components/image-block/ImageBlock.tsx +1 -0
- package/src/components/image-block/ImageCaption.tsx +6 -6
- package/src/components/image-lightbox/ImageLightbox.test.tsx +2 -1
- package/src/components/image-lightbox/internal/ImageSlide.tsx +3 -3
- package/src/components/image-lightbox/useImageLightbox.tsx +16 -16
- package/src/components/thumbnail/Thumbnail.stories.tsx +5 -3
- package/src/components/thumbnail/Thumbnail.tsx +5 -5
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.7.6-alpha.
|
|
11
|
-
"@lumx/icons": "^3.7.6-alpha.
|
|
10
|
+
"@lumx/core": "^3.7.6-alpha.13",
|
|
11
|
+
"@lumx/icons": "^3.7.6-alpha.13",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.3.2",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"build:storybook": "storybook build"
|
|
113
113
|
},
|
|
114
114
|
"sideEffects": false,
|
|
115
|
-
"version": "3.7.6-alpha.
|
|
115
|
+
"version": "3.7.6-alpha.13"
|
|
116
116
|
}
|
|
@@ -15,94 +15,105 @@ import {
|
|
|
15
15
|
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
|
|
16
16
|
import { imageArgType, PORTRAIT_IMAGES, LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
|
|
17
17
|
import { mdiFileEdit } from '@lumx/icons';
|
|
18
|
-
import {
|
|
18
|
+
import { withNestedProps } from '@lumx/react/stories/decorators/withNestedProps';
|
|
19
19
|
import { focusPoint } from '@lumx/react/stories/controls/focusPoint';
|
|
20
|
+
import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
|
|
20
21
|
|
|
21
22
|
export default {
|
|
22
23
|
title: 'LumX components/image-block/Image Block',
|
|
24
|
+
component: ImageBlock,
|
|
23
25
|
argTypes: {
|
|
24
26
|
size: getSelectArgType<ImageBlockSize>([Size.xl, Size.xxl]),
|
|
25
27
|
image: imageArgType,
|
|
26
28
|
captionPosition: getSelectArgType(ImageBlockCaptionPosition),
|
|
27
29
|
'thumbnailProps.aspectRatio': getSelectArgType(AspectRatio),
|
|
28
30
|
align: getSelectArgType<HorizontalAlignment>([Alignment.left, Alignment.center, Alignment.right]),
|
|
31
|
+
tags: { control: false },
|
|
32
|
+
actions: { control: false },
|
|
29
33
|
},
|
|
30
34
|
args: { ...ImageBlock.defaultProps, image: LANDSCAPE_IMAGES.landscape1 },
|
|
35
|
+
decorators: [withNestedProps()],
|
|
31
36
|
};
|
|
32
37
|
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
captionPosition: ImageBlockCaptionPosition.below,
|
|
41
|
-
size: Size.xxl,
|
|
42
|
-
title: 'Image block title',
|
|
43
|
-
description: 'Image block description',
|
|
38
|
+
export const WithCaptionBelow = {
|
|
39
|
+
args: {
|
|
40
|
+
captionPosition: ImageBlockCaptionPosition.below,
|
|
41
|
+
size: Size.xxl,
|
|
42
|
+
title: 'Image block title',
|
|
43
|
+
description: 'Image block description',
|
|
44
|
+
},
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
export const WithCaptionOver
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
export const WithCaptionOver = {
|
|
48
|
+
args: {
|
|
49
|
+
captionPosition: ImageBlockCaptionPosition.over,
|
|
50
|
+
size: Size.xxl,
|
|
51
|
+
title: 'Image block title',
|
|
52
|
+
description: 'Image block description',
|
|
53
|
+
},
|
|
52
54
|
};
|
|
53
55
|
|
|
54
|
-
export const WithAlign
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
export const WithAlign = {
|
|
57
|
+
args: {
|
|
58
|
+
...WithCaptionBelow.args,
|
|
59
|
+
image: LANDSCAPE_IMAGES.landscape1s200,
|
|
60
|
+
size: undefined,
|
|
61
|
+
align: Alignment.center,
|
|
62
|
+
},
|
|
60
63
|
};
|
|
61
64
|
|
|
62
|
-
export const WithTags
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</ChipGroup>
|
|
73
|
-
),
|
|
65
|
+
export const WithTags = {
|
|
66
|
+
args: {
|
|
67
|
+
size: Size.xxl,
|
|
68
|
+
tags: (
|
|
69
|
+
<ChipGroup align={Alignment.left}>
|
|
70
|
+
<Chip size={Size.s}>Tag 1</Chip>
|
|
71
|
+
<Chip size={Size.s}>Tag 2</Chip>
|
|
72
|
+
</ChipGroup>
|
|
73
|
+
),
|
|
74
|
+
},
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
export const WithActions
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
size: Size.xxl,
|
|
82
|
-
actions: <IconButton label="Edit" icon={mdiFileEdit} />,
|
|
77
|
+
export const WithActions = {
|
|
78
|
+
args: {
|
|
79
|
+
size: Size.xxl,
|
|
80
|
+
actions: <IconButton label="Edit" icon={mdiFileEdit} />,
|
|
81
|
+
},
|
|
83
82
|
};
|
|
84
83
|
|
|
85
|
-
export const WithFocusPointHorizontal
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
export const WithFocusPointHorizontal = {
|
|
85
|
+
args: {
|
|
86
|
+
size: Size.xxl,
|
|
87
|
+
'thumbnailProps.aspectRatio': AspectRatio.vertical,
|
|
88
|
+
'thumbnailProps.focusPoint.x': 1,
|
|
89
|
+
'thumbnailProps.focusPoint.y': 0,
|
|
90
|
+
},
|
|
91
|
+
argTypes: {
|
|
92
|
+
'thumbnailProps.focusPoint.x': focusPoint,
|
|
93
|
+
'thumbnailProps.focusPoint.y': focusPoint,
|
|
94
|
+
},
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
export const WithFocusPointVertical
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
export const WithFocusPointVertical = {
|
|
98
|
+
args: {
|
|
99
|
+
size: Size.xxl,
|
|
100
|
+
image: PORTRAIT_IMAGES.portrait1,
|
|
101
|
+
'thumbnailProps.aspectRatio': AspectRatio.horizontal,
|
|
102
|
+
'thumbnailProps.focusPoint.x': 0,
|
|
103
|
+
'thumbnailProps.focusPoint.y': 1,
|
|
104
|
+
},
|
|
105
|
+
argTypes: {
|
|
106
|
+
'thumbnailProps.focusPoint.x': focusPoint,
|
|
107
|
+
'thumbnailProps.focusPoint.y': focusPoint,
|
|
108
|
+
},
|
|
104
109
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
export const FullFeatured = {
|
|
111
|
+
args: {
|
|
112
|
+
...WithCaptionBelow.args,
|
|
113
|
+
...WithTags.args,
|
|
114
|
+
...WithActions.args,
|
|
115
|
+
...WithFocusPointVertical.args,
|
|
116
|
+
...WithAlign.args,
|
|
117
|
+
},
|
|
118
|
+
decorators: [withWrapper({ style: { width: 400, height: 300 } })],
|
|
108
119
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { CSSProperties, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
import { FlexBox, HorizontalAlignment, Text } from '@lumx/react';
|
|
3
|
+
import { FlexBox, HorizontalAlignment, Text, TextProps } from '@lumx/react';
|
|
4
4
|
import { HasClassName, HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type';
|
|
5
5
|
|
|
6
6
|
type As = 'div' | 'figcaption';
|
|
@@ -22,8 +22,8 @@ export type ImageCaptionProps<AS extends As = 'figcaption'> = HasTheme &
|
|
|
22
22
|
ImageCaptionMetadata & {
|
|
23
23
|
/** Alignment. */
|
|
24
24
|
align?: HorizontalAlignment;
|
|
25
|
-
/** Truncate title & description */
|
|
26
|
-
truncate?:
|
|
25
|
+
/** Truncate text on title & description (no line wrapping). */
|
|
26
|
+
truncate?: TextProps['truncate'];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/** Internal component used to render image captions */
|
|
@@ -32,7 +32,7 @@ export const ImageCaption = <AS extends As>(props: ImageCaptionProps<AS>) => {
|
|
|
32
32
|
if (!title && !description && !tags) return null;
|
|
33
33
|
|
|
34
34
|
const titleColor = { color: theme === 'dark' ? 'light' : 'dark' } as const;
|
|
35
|
-
const
|
|
35
|
+
const baseColor = { color: theme === 'dark' ? 'light' : 'dark', colorVariant: 'L2' } as const;
|
|
36
36
|
|
|
37
37
|
// Display description as string or HTML
|
|
38
38
|
const descriptionContent =
|
|
@@ -49,13 +49,13 @@ export const ImageCaption = <AS extends As>(props: ImageCaptionProps<AS>) => {
|
|
|
49
49
|
gap="regular"
|
|
50
50
|
>
|
|
51
51
|
{(title || description) && (
|
|
52
|
-
<Text as="p" truncate={truncate}>
|
|
52
|
+
<Text as="p" truncate={truncate} {...baseColor}>
|
|
53
53
|
{title && (
|
|
54
54
|
<Text as="span" typography="subtitle1" {...titleColor}>
|
|
55
55
|
{title}
|
|
56
56
|
</Text>
|
|
57
57
|
)}{' '}
|
|
58
|
-
{description && <Text as="span" typography="body1" {...
|
|
58
|
+
{description && <Text as="span" typography="body1" {...descriptionContent} />}
|
|
59
59
|
</Text>
|
|
60
60
|
)}
|
|
61
61
|
{tags && (
|
|
@@ -41,7 +41,8 @@ const queries = {
|
|
|
41
41
|
getImageLightbox: () => getByClassName(document.body, CLASSNAME),
|
|
42
42
|
queryCloseButton: (imageLightbox: HTMLElement) => within(imageLightbox).queryByRole('button', { name: 'Close' }),
|
|
43
43
|
queryImage: (imageLightbox: HTMLElement, name?: string) => within(imageLightbox).queryByRole('img', { name }),
|
|
44
|
-
queryScrollArea: (imageLightbox: HTMLElement) =>
|
|
44
|
+
queryScrollArea: (imageLightbox: HTMLElement) =>
|
|
45
|
+
queryByClassName(imageLightbox, 'lumx-image-lightbox__image-slide'),
|
|
45
46
|
queryZoomInButton: (imageLightbox: HTMLElement) => within(imageLightbox).queryByRole('button', { name: 'Zoom in' }),
|
|
46
47
|
queryZoomOutButton: (imageLightbox: HTMLElement) =>
|
|
47
48
|
within(imageLightbox).queryByRole('button', { name: 'Zoom out' }),
|
|
@@ -39,9 +39,9 @@ export const ImageSlide = React.memo((props: ImageSlideProps) => {
|
|
|
39
39
|
// Get image size
|
|
40
40
|
const imgRef = React.useRef<HTMLImageElement>(null);
|
|
41
41
|
const imageSize = useImageSize(imgRef, () => {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
return
|
|
42
|
+
const width = Number.parseInt(imgProps?.width as any, 10);
|
|
43
|
+
const height = Number.parseInt(imgProps?.height as any, 10);
|
|
44
|
+
return width && height ? { width, height } : null;
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
// Calculate new image size with scale
|
|
@@ -36,27 +36,24 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
36
36
|
* */
|
|
37
37
|
getTriggerProps: (options?: TriggerOptions) => { onClick: React.MouseEventHandler; ref: React.Ref<any> };
|
|
38
38
|
/** Props to forward to the ImageLightbox */
|
|
39
|
-
imageLightboxProps:
|
|
39
|
+
imageLightboxProps: ManagedProps & P;
|
|
40
40
|
} {
|
|
41
41
|
const { images = [], ...otherProps } = initialProps;
|
|
42
42
|
|
|
43
|
-
const basePropsRef = React.useRef(EMPTY_PROPS as P & ManagedProps);
|
|
44
|
-
React.useEffect(() => {
|
|
45
|
-
basePropsRef.current = { ...EMPTY_PROPS, ...otherProps } as P & ManagedProps;
|
|
46
|
-
}, [otherProps]);
|
|
47
|
-
|
|
48
43
|
const imagesPropsRef = React.useRef(images);
|
|
49
44
|
React.useEffect(() => {
|
|
50
45
|
imagesPropsRef.current = images.map((props) => ({ imgRef: React.createRef(), ...props }));
|
|
51
46
|
}, [images]);
|
|
52
47
|
|
|
53
48
|
const currentImageRef = React.useRef<HTMLImageElement>(null);
|
|
54
|
-
const [imageLightboxProps, setImageLightboxProps] = React.useState
|
|
49
|
+
const [imageLightboxProps, setImageLightboxProps] = React.useState(
|
|
50
|
+
() => ({ ...EMPTY_PROPS, ...otherProps }) as ManagedProps & P,
|
|
51
|
+
);
|
|
55
52
|
|
|
56
53
|
const getTriggerProps = React.useMemo(() => {
|
|
57
54
|
const triggerImageRefs: Record<number, React.RefObject<HTMLImageElement>> = {};
|
|
58
55
|
|
|
59
|
-
async function
|
|
56
|
+
async function close() {
|
|
60
57
|
const currentImage = currentImageRef.current;
|
|
61
58
|
if (!currentImage) {
|
|
62
59
|
return;
|
|
@@ -67,8 +64,8 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
67
64
|
|
|
68
65
|
await startViewTransition({
|
|
69
66
|
changes() {
|
|
70
|
-
// Close lightbox
|
|
71
|
-
setImageLightboxProps((
|
|
67
|
+
// Close lightbox
|
|
68
|
+
setImageLightboxProps((prevProps) => ({ ...prevProps, isOpen: false }));
|
|
72
69
|
},
|
|
73
70
|
// Morph from the image in lightbox to the image in trigger
|
|
74
71
|
viewTransitionName: {
|
|
@@ -79,7 +76,7 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
async function
|
|
79
|
+
async function open(triggerElement: HTMLElement, { activeImageIndex }: TriggerOptions = {}) {
|
|
83
80
|
// If we find an image inside the trigger, animate it in transition with the opening image
|
|
84
81
|
const triggerImage = triggerImageRefs[activeImageIndex as any]?.current || findImage(triggerElement);
|
|
85
82
|
|
|
@@ -94,15 +91,18 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
94
91
|
await startViewTransition({
|
|
95
92
|
changes: () => {
|
|
96
93
|
// Open lightbox with setup props
|
|
97
|
-
setImageLightboxProps({
|
|
98
|
-
...
|
|
94
|
+
setImageLightboxProps((prevProps) => ({
|
|
95
|
+
...prevProps,
|
|
99
96
|
activeImageRef: currentImageRef,
|
|
100
97
|
parentElement: { current: triggerElement },
|
|
101
98
|
isOpen: true,
|
|
102
|
-
onClose:
|
|
99
|
+
onClose: () => {
|
|
100
|
+
close();
|
|
101
|
+
prevProps?.onClose?.();
|
|
102
|
+
},
|
|
103
103
|
images: imagesWithFallbackSize,
|
|
104
104
|
activeImageIndex: activeImageIndex || 0,
|
|
105
|
-
});
|
|
105
|
+
}));
|
|
106
106
|
},
|
|
107
107
|
// Morph from the image in trigger to the image in lightbox
|
|
108
108
|
viewTransitionName: {
|
|
@@ -121,7 +121,7 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
onClick(e: React.MouseEvent) {
|
|
124
|
-
|
|
124
|
+
open(e.target as HTMLElement, options);
|
|
125
125
|
},
|
|
126
126
|
}));
|
|
127
127
|
}, []);
|
|
@@ -428,10 +428,12 @@ export const LoadingPlaceholderImage = () => {
|
|
|
428
428
|
<Thumbnail alt="Small image" imgRef={imgRef} image="https://picsum.photos/id/15/128/85" />
|
|
429
429
|
{isShown && (
|
|
430
430
|
<Thumbnail
|
|
431
|
-
loadingPlaceholderImageRef={imgRef}
|
|
432
|
-
style={{ maxWidth: 300 }}
|
|
433
|
-
alt="Large image"
|
|
434
431
|
image="https://picsum.photos/id/15/2500/1667"
|
|
432
|
+
alt="Large image"
|
|
433
|
+
// Loading placeholder image
|
|
434
|
+
loadingPlaceholderImageRef={imgRef}
|
|
435
|
+
// Reserve space
|
|
436
|
+
imgProps={{ width: 2500, height: 1667 }}
|
|
435
437
|
/>
|
|
436
438
|
)}
|
|
437
439
|
</FlexBox>
|
|
@@ -166,12 +166,10 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
166
166
|
const loadingPlaceholderImage =
|
|
167
167
|
(isLoading && loadingPlaceholderImageRef?.current?.complete && loadingPlaceholderImageRef?.current) ||
|
|
168
168
|
undefined;
|
|
169
|
+
|
|
170
|
+
// Set loading placeholder image as background
|
|
169
171
|
const loadingStyle = loadingPlaceholderImage
|
|
170
|
-
? {
|
|
171
|
-
backgroundImage: `url(${loadingPlaceholderImage.src})`,
|
|
172
|
-
minWidth: loadingPlaceholderImage.naturalWidth,
|
|
173
|
-
minHeight: loadingPlaceholderImage.naturalHeight,
|
|
174
|
-
}
|
|
172
|
+
? { backgroundImage: `url(${loadingPlaceholderImage.src})` }
|
|
175
173
|
: undefined;
|
|
176
174
|
|
|
177
175
|
return (
|
|
@@ -202,6 +200,8 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
202
200
|
<span className={`${CLASSNAME}__background`}>
|
|
203
201
|
<img
|
|
204
202
|
{...imgProps}
|
|
203
|
+
width={imgProps?.width || loadingPlaceholderImage?.naturalWidth}
|
|
204
|
+
height={imgProps?.height || loadingPlaceholderImage?.naturalHeight}
|
|
205
205
|
style={{
|
|
206
206
|
...imgProps?.style,
|
|
207
207
|
...imageErrorStyle,
|