@lumx/react 3.7.6-alpha.0 → 3.7.6-alpha.10
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 +15 -12
- package/index.js +225 -182
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/image-block/ImageBlock.tsx +15 -2
- package/src/components/image-block/ImageCaption.tsx +17 -22
- package/src/components/image-lightbox/ImageLightbox.stories.tsx +10 -8
- package/src/components/image-lightbox/internal/ImageSlide.tsx +13 -5
- package/src/components/image-lightbox/internal/ImageSlideshow.tsx +21 -6
- package/src/components/image-lightbox/types.ts +14 -13
- package/src/components/image-lightbox/useImageLightbox.tsx +30 -22
- package/src/components/thumbnail/Thumbnail.stories.tsx +29 -0
- package/src/components/thumbnail/Thumbnail.tsx +16 -0
- package/src/hooks/useElementSizeDependentOfWindowSize.ts +2 -2
- package/src/stories/generated/ImageLightbox/Demos.stories.tsx +6 -0
- package/src/utils/findImage.tsx +2 -2
- package/src/components/image-block/constants.ts +0 -11
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.10",
|
|
11
|
+
"@lumx/icons": "^3.7.6-alpha.10",
|
|
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.10"
|
|
116
116
|
}
|
|
@@ -3,12 +3,12 @@ import React, { forwardRef, ReactNode } from 'react';
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
|
|
5
5
|
import { Alignment, HorizontalAlignment, Size, Theme, Thumbnail } from '@lumx/react';
|
|
6
|
+
|
|
6
7
|
import { Comp, GenericProps, HasTheme, ValueOf } from '@lumx/react/utils/type';
|
|
7
|
-
import { handleBasicClasses } from '@lumx/react/utils/className';
|
|
8
|
+
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
8
9
|
|
|
9
10
|
import { ThumbnailProps } from '../thumbnail/Thumbnail';
|
|
10
11
|
import { ImageCaption, ImageCaptionMetadata } from './ImageCaption';
|
|
11
|
-
import { CLASSNAME, COMPONENT_NAME } from './constants';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Image block variants.
|
|
@@ -46,6 +46,16 @@ export interface ImageBlockProps extends GenericProps, HasTheme, ImageCaptionMet
|
|
|
46
46
|
thumbnailProps?: Omit<ThumbnailProps, 'image' | 'size' | 'theme' | 'align' | 'fillHeight'>;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Component display name.
|
|
51
|
+
*/
|
|
52
|
+
const COMPONENT_NAME = 'ImageBlock';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Component default class name and class prefix.
|
|
56
|
+
*/
|
|
57
|
+
const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* Component default props.
|
|
51
61
|
*/
|
|
@@ -107,11 +117,14 @@ export const ImageBlock: Comp<ImageBlockProps, HTMLDivElement> = forwardRef((pro
|
|
|
107
117
|
alt={(alt || title) as string}
|
|
108
118
|
/>
|
|
109
119
|
<ImageCaption
|
|
120
|
+
className={`${CLASSNAME}__wrapper`}
|
|
121
|
+
theme={theme}
|
|
110
122
|
title={title}
|
|
111
123
|
description={description}
|
|
112
124
|
tags={tags}
|
|
113
125
|
captionStyle={captionStyle}
|
|
114
126
|
align={align}
|
|
127
|
+
truncate={captionPosition === 'over'}
|
|
115
128
|
/>
|
|
116
129
|
{actions && <div className={`${CLASSNAME}__actions`}>{actions}</div>}
|
|
117
130
|
</figure>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React, { CSSProperties, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
import { FlexBox, HorizontalAlignment, Text } from '@lumx/react';
|
|
4
|
-
import { HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type';
|
|
5
|
-
import { CLASSNAME } from './constants';
|
|
4
|
+
import { HasClassName, HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type';
|
|
6
5
|
|
|
7
6
|
type As = 'div' | 'figcaption';
|
|
8
7
|
|
|
@@ -18,19 +17,22 @@ export type ImageCaptionMetadata = {
|
|
|
18
17
|
};
|
|
19
18
|
|
|
20
19
|
export type ImageCaptionProps<AS extends As = 'figcaption'> = HasTheme &
|
|
20
|
+
HasClassName &
|
|
21
21
|
HasPolymorphicAs<AS> &
|
|
22
22
|
ImageCaptionMetadata & {
|
|
23
23
|
/** Alignment. */
|
|
24
24
|
align?: HorizontalAlignment;
|
|
25
|
+
/** Truncate title & description */
|
|
26
|
+
truncate?: boolean;
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
/** Internal component used to render image captions */
|
|
28
30
|
export const ImageCaption = <AS extends As>(props: ImageCaptionProps<AS>) => {
|
|
29
|
-
const { theme, as = 'figcaption', title, description, tags, captionStyle, align } = props;
|
|
31
|
+
const { className, theme, as = 'figcaption', title, description, tags, captionStyle, align, truncate } = props;
|
|
30
32
|
if (!title && !description && !tags) return null;
|
|
31
33
|
|
|
32
|
-
const titleColor = theme === 'dark' ?
|
|
33
|
-
const descriptionColor = theme === 'dark' ?
|
|
34
|
+
const titleColor = { color: theme === 'dark' ? 'light' : 'dark' } as const;
|
|
35
|
+
const descriptionColor = { color: theme === 'dark' ? 'light' : 'dark', colorVariant: 'L2' } as const;
|
|
34
36
|
|
|
35
37
|
// Display description as string or HTML
|
|
36
38
|
const descriptionContent =
|
|
@@ -39,32 +41,25 @@ export const ImageCaption = <AS extends As>(props: ImageCaptionProps<AS>) => {
|
|
|
39
41
|
return (
|
|
40
42
|
<FlexBox
|
|
41
43
|
as={as}
|
|
42
|
-
className={
|
|
44
|
+
className={className}
|
|
43
45
|
style={captionStyle}
|
|
44
|
-
orientation="
|
|
46
|
+
orientation="vertical"
|
|
45
47
|
vAlign={align}
|
|
48
|
+
hAlign={align === 'center' ? align : undefined}
|
|
49
|
+
gap="regular"
|
|
46
50
|
>
|
|
47
51
|
{(title || description) && (
|
|
48
|
-
<
|
|
52
|
+
<Text as="p" truncate={truncate}>
|
|
49
53
|
{title && (
|
|
50
|
-
<Text as="span"
|
|
54
|
+
<Text as="span" typography="subtitle1" {...titleColor}>
|
|
51
55
|
{title}
|
|
52
56
|
</Text>
|
|
53
|
-
)}
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
{description && (
|
|
57
|
-
<Text
|
|
58
|
-
as="span"
|
|
59
|
-
className={`${CLASSNAME}__description`}
|
|
60
|
-
{...descriptionColor}
|
|
61
|
-
{...descriptionContent}
|
|
62
|
-
/>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
57
|
+
)}{' '}
|
|
58
|
+
{description && <Text as="span" typography="body1" {...descriptionColor} {...descriptionContent} />}
|
|
59
|
+
</Text>
|
|
65
60
|
)}
|
|
66
61
|
{tags && (
|
|
67
|
-
<FlexBox orientation="horizontal" vAlign={align}
|
|
62
|
+
<FlexBox orientation="horizontal" vAlign={align}>
|
|
68
63
|
{tags}
|
|
69
64
|
</FlexBox>
|
|
70
65
|
)}
|
|
@@ -119,15 +119,17 @@ export const MultipleImagesWithZoom = {
|
|
|
119
119
|
export const WithButtonTrigger = {
|
|
120
120
|
decorators: [
|
|
121
121
|
(Story: any, { args }: any) => {
|
|
122
|
-
const { getTriggerProps, imageLightboxProps } = ImageLightbox.useImageLightbox(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const { getTriggerProps, imageLightboxProps } = ImageLightbox.useImageLightbox({
|
|
123
|
+
images: [
|
|
124
|
+
{ image: IMAGES.portrait1s200, alt: 'Image 1' },
|
|
125
|
+
{ image: IMAGES.landscape1s200, alt: 'Image 2' },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
126
128
|
return (
|
|
127
129
|
<>
|
|
128
130
|
<Story args={{ ...args, ...imageLightboxProps }} />
|
|
129
|
-
<Button {...(getTriggerProps(0) as any)}>Image 1</Button>
|
|
130
|
-
<Button {...(getTriggerProps(1) as any)}>Image 2</Button>
|
|
131
|
+
<Button {...(getTriggerProps({ activeImageIndex: 0 }) as any)}>Image 1</Button>
|
|
132
|
+
<Button {...(getTriggerProps({ activeImageIndex: 1 }) as any)}>Image 2</Button>
|
|
131
133
|
</>
|
|
132
134
|
);
|
|
133
135
|
},
|
|
@@ -143,14 +145,14 @@ export const WithMosaicTrigger = {
|
|
|
143
145
|
args: { ...SLIDESHOW_PROPS, ...ZOOM_PROPS },
|
|
144
146
|
decorators: [
|
|
145
147
|
(Story: any, { args }: any) => {
|
|
146
|
-
const { getTriggerProps, imageLightboxProps } = ImageLightbox.useImageLightbox(MULTIPLE_IMAGES);
|
|
148
|
+
const { getTriggerProps, imageLightboxProps } = ImageLightbox.useImageLightbox({ images: MULTIPLE_IMAGES });
|
|
147
149
|
return (
|
|
148
150
|
<>
|
|
149
151
|
<Story args={{ ...args, ...imageLightboxProps }} />
|
|
150
152
|
<Mosaic
|
|
151
153
|
thumbnails={MULTIPLE_IMAGES.map((image, index) => ({
|
|
152
154
|
...image,
|
|
153
|
-
...getTriggerProps(index),
|
|
155
|
+
...getTriggerProps({ activeImageIndex: index }),
|
|
154
156
|
}))}
|
|
155
157
|
/>
|
|
156
158
|
</>
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
import isEqual from 'lodash/isEqual';
|
|
3
4
|
import { SlideshowItem, Thumbnail } from '@lumx/react';
|
|
4
5
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
5
6
|
import { useElementSizeDependentOfWindowSize } from '@lumx/react/hooks/useElementSizeDependentOfWindowSize';
|
|
6
7
|
import { useImageSize } from '@lumx/react/hooks/useImageSize';
|
|
7
|
-
|
|
8
8
|
import { getPrefersReducedMotion } from '@lumx/react/utils/getPrefersReducedMotion';
|
|
9
|
+
|
|
9
10
|
import { CLASSNAME } from '../constants';
|
|
10
11
|
import { usePointerZoom } from './usePointerZoom';
|
|
11
12
|
import { useAnimateScroll } from './useAnimateScroll';
|
|
12
|
-
import type {
|
|
13
|
+
import type { ImageProps } from '../types';
|
|
13
14
|
|
|
14
|
-
export interface ImageSlideProps
|
|
15
|
+
export interface ImageSlideProps {
|
|
16
|
+
image: ImageProps;
|
|
15
17
|
isActive?: boolean;
|
|
16
18
|
scale?: number;
|
|
17
19
|
onScaleChange?: (value: number) => void;
|
|
@@ -19,7 +21,12 @@ export interface ImageSlideProps extends InheritedThumbnailProps {
|
|
|
19
21
|
|
|
20
22
|
/** Internal image slide component for ImageLightbox */
|
|
21
23
|
export const ImageSlide = React.memo((props: ImageSlideProps) => {
|
|
22
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
isActive,
|
|
26
|
+
scale,
|
|
27
|
+
onScaleChange,
|
|
28
|
+
image: { image, imgRef: propImgRef, imgProps, alt, loadingPlaceholderImageRef },
|
|
29
|
+
} = props;
|
|
23
30
|
|
|
24
31
|
// Get scroll area size
|
|
25
32
|
const scrollAreaRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -93,7 +100,8 @@ export const ImageSlide = React.memo((props: ImageSlideProps) => {
|
|
|
93
100
|
transition: scale && !isPointerZooming && !getPrefersReducedMotion() ? 'all 250ms' : undefined,
|
|
94
101
|
},
|
|
95
102
|
}}
|
|
103
|
+
loadingPlaceholderImageRef={loadingPlaceholderImageRef}
|
|
96
104
|
/>
|
|
97
105
|
</SlideshowItem>
|
|
98
106
|
);
|
|
99
|
-
});
|
|
107
|
+
}, isEqual);
|
|
@@ -4,12 +4,13 @@ import { mdiMagnifyMinusOutline, mdiMagnifyPlusOutline } from '@lumx/icons';
|
|
|
4
4
|
import { FlexBox, IconButton, Slides, SlideshowControls } from '@lumx/react';
|
|
5
5
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
6
6
|
|
|
7
|
+
import memoize from 'lodash/memoize';
|
|
7
8
|
import { ImageCaption } from '../../image-block/ImageCaption';
|
|
8
9
|
import { CLASSNAME } from '../constants';
|
|
9
|
-
import type { ImagesProps, InheritedSlideShowProps,
|
|
10
|
+
import type { ImagesProps, InheritedSlideShowProps, ZoomButtonProps } from '../types';
|
|
10
11
|
import { ImageSlide } from './ImageSlide';
|
|
11
12
|
|
|
12
|
-
export interface ImageSlideshowProps extends InheritedSlideShowProps,
|
|
13
|
+
export interface ImageSlideshowProps extends InheritedSlideShowProps, ZoomButtonProps, ImagesProps {
|
|
13
14
|
currentPaginationItemRef?: React.Ref<HTMLButtonElement>;
|
|
14
15
|
footerRef?: React.Ref<HTMLDivElement>;
|
|
15
16
|
}
|
|
@@ -110,6 +111,18 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
|
|
|
110
111
|
</>
|
|
111
112
|
);
|
|
112
113
|
|
|
114
|
+
const getImgRef = React.useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
memoize(
|
|
117
|
+
(index: number, isActive: boolean) => {
|
|
118
|
+
return mergeRefs(images?.[index].imgRef, isActive ? activeImageRef : undefined);
|
|
119
|
+
},
|
|
120
|
+
// memoize based on both arguments
|
|
121
|
+
(...args) => args.join(),
|
|
122
|
+
),
|
|
123
|
+
[images, activeImageRef],
|
|
124
|
+
);
|
|
125
|
+
|
|
113
126
|
return (
|
|
114
127
|
<>
|
|
115
128
|
<Slides
|
|
@@ -122,15 +135,17 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
|
|
|
122
135
|
slidesId={slideshowSlidesId}
|
|
123
136
|
toggleAutoPlay={toggleAutoPlay}
|
|
124
137
|
>
|
|
125
|
-
{images.map(({ image, imgRef, ...
|
|
138
|
+
{images.map(({ image, imgRef, ...imageProps }, index) => {
|
|
126
139
|
const isActive = index === activeIndex;
|
|
127
140
|
return (
|
|
128
141
|
<ImageSlide
|
|
129
|
-
{...props}
|
|
130
142
|
isActive={isActive}
|
|
131
143
|
key={image}
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
image={{
|
|
145
|
+
...imageProps,
|
|
146
|
+
image,
|
|
147
|
+
imgRef: getImgRef(index, isActive),
|
|
148
|
+
}}
|
|
134
149
|
scale={isActive ? scale : undefined}
|
|
135
150
|
onScaleChange={onScaleChange}
|
|
136
151
|
/>
|
|
@@ -4,25 +4,26 @@ import type { IconButtonProps, LightboxProps, SlideshowProps, ThumbnailProps } f
|
|
|
4
4
|
import type { HasClassName } from '@lumx/react/utils/type';
|
|
5
5
|
import type { ImageCaptionMetadata } from '@lumx/react/components/image-block/ImageCaption';
|
|
6
6
|
|
|
7
|
-
export type InheritedSlideShowProps = Pick<
|
|
8
|
-
SlideshowProps,
|
|
9
|
-
'activeIndex' | 'slideshowControlsProps' | 'slideGroupLabel'
|
|
10
|
-
>;
|
|
7
|
+
export type InheritedSlideShowProps = Pick<SlideshowProps, 'slideshowControlsProps' | 'slideGroupLabel'>;
|
|
11
8
|
|
|
12
|
-
export interface
|
|
13
|
-
/** */
|
|
9
|
+
export interface ZoomButtonProps {
|
|
10
|
+
/** Zoom in button props */
|
|
14
11
|
zoomInButtonProps?: IconButtonProps;
|
|
12
|
+
/** Zoom out button props */
|
|
15
13
|
zoomOutButtonProps?: IconButtonProps;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
export type InheritedThumbnailProps = Pick<
|
|
16
|
+
export type InheritedThumbnailProps = Pick<
|
|
17
|
+
ThumbnailProps,
|
|
18
|
+
'image' | 'alt' | 'imgProps' | 'imgRef' | 'loadingPlaceholderImageRef'
|
|
19
|
+
>;
|
|
19
20
|
|
|
20
21
|
export type InheritedImageMetadata = Pick<ImageCaptionMetadata, 'title' | 'description' | 'tags'>;
|
|
21
22
|
|
|
22
23
|
export type ImageProps = InheritedThumbnailProps & InheritedImageMetadata;
|
|
23
24
|
|
|
24
25
|
export interface ImagesProps {
|
|
25
|
-
/** Index of the active
|
|
26
|
+
/** Index of the active image to show on open */
|
|
26
27
|
activeImageIndex?: number;
|
|
27
28
|
/** List of images to display */
|
|
28
29
|
images: Array<ImageProps>;
|
|
@@ -30,9 +31,10 @@ export interface ImagesProps {
|
|
|
30
31
|
activeImageRef?: React.Ref<HTMLImageElement>;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export type InheritedLightboxProps = Pick<
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
export type InheritedLightboxProps = Pick<
|
|
35
|
+
LightboxProps,
|
|
36
|
+
'isOpen' | 'parentElement' | 'onClose' | 'closeButtonProps' | 'aria-label' | 'aria-labelledby'
|
|
37
|
+
>;
|
|
36
38
|
|
|
37
39
|
export type ForwardedProps = React.ComponentPropsWithoutRef<'div'>;
|
|
38
40
|
|
|
@@ -41,9 +43,8 @@ export type ForwardedProps = React.ComponentPropsWithoutRef<'div'>;
|
|
|
41
43
|
*/
|
|
42
44
|
export interface ImageLightboxProps
|
|
43
45
|
extends HasClassName,
|
|
44
|
-
|
|
46
|
+
ZoomButtonProps,
|
|
45
47
|
ImagesProps,
|
|
46
48
|
InheritedSlideShowProps,
|
|
47
49
|
InheritedLightboxProps,
|
|
48
|
-
InheritedAriaAttributes,
|
|
49
50
|
ForwardedProps {}
|
|
@@ -16,6 +16,8 @@ type ManagedProps = Pick<
|
|
|
16
16
|
|
|
17
17
|
const EMPTY_PROPS: ManagedProps = { isOpen: false, images: [], parentElement: React.createRef() };
|
|
18
18
|
|
|
19
|
+
type TriggerOptions = Pick<ImageLightboxProps, 'activeImageIndex'>;
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* Set up an ImageLightbox with images and triggers.
|
|
21
23
|
*
|
|
@@ -23,24 +25,33 @@ const EMPTY_PROPS: ManagedProps = { isOpen: false, images: [], parentElement: Re
|
|
|
23
25
|
* - Associate a trigger with an image to display on open
|
|
24
26
|
* - Automatically provide a view transition between an image trigger and the displayed image on open & close
|
|
25
27
|
*
|
|
26
|
-
* @param
|
|
28
|
+
* @param initialProps Images to display in the image lightbox
|
|
27
29
|
*/
|
|
28
|
-
export function useImageLightbox
|
|
30
|
+
export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
|
|
31
|
+
initialProps: P,
|
|
32
|
+
): {
|
|
29
33
|
/**
|
|
30
34
|
* Generates trigger props
|
|
31
35
|
* @param index Provide an index to choose which image to display when the image lightbox opens.
|
|
32
36
|
* */
|
|
33
|
-
getTriggerProps: (
|
|
37
|
+
getTriggerProps: (options?: TriggerOptions) => { onClick: React.MouseEventHandler; ref: React.Ref<any> };
|
|
34
38
|
/** Props to forward to the ImageLightbox */
|
|
35
|
-
imageLightboxProps: ManagedProps;
|
|
39
|
+
imageLightboxProps: P & ManagedProps;
|
|
36
40
|
} {
|
|
41
|
+
const { images = [], ...otherProps } = initialProps;
|
|
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
|
+
|
|
37
48
|
const imagesPropsRef = React.useRef(images);
|
|
38
49
|
React.useEffect(() => {
|
|
39
50
|
imagesPropsRef.current = images.map((props) => ({ imgRef: React.createRef(), ...props }));
|
|
40
51
|
}, [images]);
|
|
41
52
|
|
|
42
53
|
const currentImageRef = React.useRef<HTMLImageElement>(null);
|
|
43
|
-
const [imageLightboxProps, setImageLightboxProps] = React.useState<ManagedProps>(
|
|
54
|
+
const [imageLightboxProps, setImageLightboxProps] = React.useState<P & ManagedProps>(basePropsRef.current);
|
|
44
55
|
|
|
45
56
|
const getTriggerProps = React.useMemo(() => {
|
|
46
57
|
const triggerImageRefs: Record<number, React.RefObject<HTMLImageElement>> = {};
|
|
@@ -57,31 +68,25 @@ export function useImageLightbox(images: ImageLightboxProps['images'] = []): {
|
|
|
57
68
|
await startViewTransition({
|
|
58
69
|
changes() {
|
|
59
70
|
// Close lightbox with reset empty props
|
|
60
|
-
setImageLightboxProps(({ parentElement }) => ({ ...
|
|
71
|
+
setImageLightboxProps(({ parentElement }) => ({ ...basePropsRef.current, parentElement }));
|
|
61
72
|
},
|
|
62
73
|
// Morph from the image in lightbox to the image in trigger
|
|
63
74
|
viewTransitionName: {
|
|
64
75
|
source: currentImageRef,
|
|
65
|
-
//source: imageIsVisible ? currentImageRef : null,
|
|
66
76
|
target: triggerImageRefs[currentIndex],
|
|
67
77
|
name: CLASSNAME,
|
|
68
78
|
},
|
|
69
79
|
});
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
async function openLightbox(triggerElement: HTMLElement,
|
|
82
|
+
async function openLightbox(triggerElement: HTMLElement, { activeImageIndex }: TriggerOptions = {}) {
|
|
73
83
|
// If we find an image inside the trigger, animate it in transition with the opening image
|
|
74
|
-
const triggerImage = triggerImageRefs[
|
|
84
|
+
const triggerImage = triggerImageRefs[activeImageIndex as any]?.current || findImage(triggerElement);
|
|
75
85
|
|
|
76
|
-
// Inject the trigger image
|
|
86
|
+
// Inject the trigger image as loading placeholder for better loading state
|
|
77
87
|
const imagesWithFallbackSize = imagesPropsRef.current.map((image, idx) => {
|
|
78
|
-
if (triggerImage && idx ===
|
|
79
|
-
|
|
80
|
-
...image.imgProps,
|
|
81
|
-
height: triggerImage.naturalHeight,
|
|
82
|
-
width: triggerImage.naturalWidth,
|
|
83
|
-
};
|
|
84
|
-
return { ...image, imgProps };
|
|
88
|
+
if (triggerImage && idx === activeImageIndex && !image.loadingPlaceholderImageRef) {
|
|
89
|
+
return { ...image, loadingPlaceholderImageRef: { current: triggerImage } };
|
|
85
90
|
}
|
|
86
91
|
return image;
|
|
87
92
|
});
|
|
@@ -90,12 +95,13 @@ export function useImageLightbox(images: ImageLightboxProps['images'] = []): {
|
|
|
90
95
|
changes: () => {
|
|
91
96
|
// Open lightbox with setup props
|
|
92
97
|
setImageLightboxProps({
|
|
98
|
+
...basePropsRef.current,
|
|
93
99
|
activeImageRef: currentImageRef,
|
|
94
100
|
parentElement: { current: triggerElement },
|
|
95
101
|
isOpen: true,
|
|
96
102
|
onClose: closeLightbox,
|
|
97
103
|
images: imagesWithFallbackSize,
|
|
98
|
-
activeImageIndex:
|
|
104
|
+
activeImageIndex: activeImageIndex || 0,
|
|
99
105
|
});
|
|
100
106
|
},
|
|
101
107
|
// Morph from the image in trigger to the image in lightbox
|
|
@@ -107,13 +113,15 @@ export function useImageLightbox(images: ImageLightboxProps['images'] = []): {
|
|
|
107
113
|
});
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
return memoize((
|
|
116
|
+
return memoize((options?: TriggerOptions) => ({
|
|
111
117
|
ref(element: HTMLElement | null) {
|
|
112
|
-
|
|
113
|
-
if (
|
|
118
|
+
// Track trigger image ref if any
|
|
119
|
+
if (options?.activeImageIndex !== undefined && element) {
|
|
120
|
+
triggerImageRefs[options.activeImageIndex] = { current: findImage(element) };
|
|
121
|
+
}
|
|
114
122
|
},
|
|
115
123
|
onClick(e: React.MouseEvent) {
|
|
116
|
-
openLightbox(e.target as HTMLElement,
|
|
124
|
+
openLightbox(e.target as HTMLElement, options);
|
|
117
125
|
},
|
|
118
126
|
}));
|
|
119
127
|
}, []);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Alignment,
|
|
6
6
|
AspectRatio,
|
|
7
7
|
Badge,
|
|
8
|
+
Button,
|
|
8
9
|
FlexBox,
|
|
9
10
|
GridColumn,
|
|
10
11
|
Icon,
|
|
@@ -411,3 +412,31 @@ export const ObjectFit = {
|
|
|
411
412
|
withWrapper({ maxColumns: 3, itemMinWidth: 350 }, GridColumn),
|
|
412
413
|
],
|
|
413
414
|
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Demonstrate loading a small image and then use it as the loading placeholder image when loading a bigger image
|
|
418
|
+
*/
|
|
419
|
+
export const LoadingPlaceholderImage = () => {
|
|
420
|
+
const [isShown, setShown] = React.useState(false);
|
|
421
|
+
const imgRef = React.useRef() as React.RefObject<HTMLImageElement>;
|
|
422
|
+
return (
|
|
423
|
+
<>
|
|
424
|
+
<Button onClick={() => setShown((shown) => !shown)}>
|
|
425
|
+
Display bigger image using the small image as a placeholder
|
|
426
|
+
</Button>
|
|
427
|
+
<FlexBox orientation="horizontal">
|
|
428
|
+
<Thumbnail alt="Small image" imgRef={imgRef} image="https://picsum.photos/id/15/128/85" />
|
|
429
|
+
{isShown && (
|
|
430
|
+
<Thumbnail
|
|
431
|
+
loadingPlaceholderImageRef={imgRef}
|
|
432
|
+
style={{ maxWidth: 300 }}
|
|
433
|
+
alt="Large image"
|
|
434
|
+
image="https://picsum.photos/id/15/2500/1667"
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
</FlexBox>
|
|
438
|
+
</>
|
|
439
|
+
);
|
|
440
|
+
};
|
|
441
|
+
// Disables Chromatic snapshot (not relevant for this story).
|
|
442
|
+
LoadingPlaceholderImage.parameters = { chromatic: { disable: true } };
|
|
@@ -58,6 +58,8 @@ export interface ThumbnailProps extends GenericProps, HasTheme {
|
|
|
58
58
|
size?: ThumbnailSize;
|
|
59
59
|
/** Image loading mode. */
|
|
60
60
|
loading?: ImgHTMLProps['loading'];
|
|
61
|
+
/** Ref of an existing placeholder image to display while loading. */
|
|
62
|
+
loadingPlaceholderImageRef?: React.RefObject<HTMLImageElement>;
|
|
61
63
|
/** On click callback. */
|
|
62
64
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
63
65
|
/** On key press callback. */
|
|
@@ -115,6 +117,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
115
117
|
isLoading: isLoadingProp,
|
|
116
118
|
objectFit,
|
|
117
119
|
loading,
|
|
120
|
+
loadingPlaceholderImageRef,
|
|
118
121
|
size,
|
|
119
122
|
theme,
|
|
120
123
|
variant,
|
|
@@ -159,6 +162,18 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
159
162
|
wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt;
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
// If we have a loading placeholder image that is really loaded (complete)
|
|
166
|
+
const loadingPlaceholderImage =
|
|
167
|
+
(isLoading && loadingPlaceholderImageRef?.current?.complete && loadingPlaceholderImageRef?.current) ||
|
|
168
|
+
undefined;
|
|
169
|
+
const loadingStyle = loadingPlaceholderImage
|
|
170
|
+
? {
|
|
171
|
+
backgroundImage: `url(${loadingPlaceholderImage.src})`,
|
|
172
|
+
minWidth: loadingPlaceholderImage.naturalWidth,
|
|
173
|
+
minHeight: loadingPlaceholderImage.naturalHeight,
|
|
174
|
+
}
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
162
177
|
return (
|
|
163
178
|
<Wrapper
|
|
164
179
|
{...wrapperProps}
|
|
@@ -191,6 +206,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
|
|
|
191
206
|
...imgProps?.style,
|
|
192
207
|
...imageErrorStyle,
|
|
193
208
|
...focusPointStyle,
|
|
209
|
+
...loadingStyle,
|
|
194
210
|
}}
|
|
195
211
|
ref={mergeRefs(setImgElement, propImgRef)}
|
|
196
212
|
className={classNames(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { RectSize } from '@lumx/react/utils/type';
|
|
4
3
|
import throttle from 'lodash/throttle';
|
|
4
|
+
import { RectSize } from '@lumx/react/utils/type';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Observe element size (only works if it's size depends on the window size).
|
|
@@ -18,7 +18,7 @@ export function useElementSizeDependentOfWindowSize(
|
|
|
18
18
|
const updateSize = React.useMemo(
|
|
19
19
|
() =>
|
|
20
20
|
throttle(() => {
|
|
21
|
-
const newSize = elementRef
|
|
21
|
+
const newSize = elementRef.current?.getBoundingClientRect();
|
|
22
22
|
if (newSize) setSize(newSize);
|
|
23
23
|
}, 10),
|
|
24
24
|
[elementRef],
|
package/src/utils/findImage.tsx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Find image in element including the element */
|
|
2
|
-
export const findImage = (element: HTMLElement | null) =>
|
|
3
|
-
element?.matches('img') ? (element as HTMLImageElement) : element?.querySelector('img');
|
|
2
|
+
export const findImage = (element: HTMLElement | null): HTMLImageElement | null =>
|
|
3
|
+
element?.matches('img') ? (element as HTMLImageElement) : element?.querySelector('img') || null;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { getRootClassName } from '@lumx/react/utils/className';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Component display name.
|
|
5
|
-
*/
|
|
6
|
-
export const COMPONENT_NAME = 'ImageBlock';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Component default class name and class prefix.
|
|
10
|
-
*/
|
|
11
|
-
export const CLASSNAME = getRootClassName(COMPONENT_NAME);
|