@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/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.0",
11
- "@lumx/icons": "^3.7.6-alpha.0",
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.0"
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' ? ({ color: 'light' } as const) : undefined;
33
- const descriptionColor = theme === 'dark' ? ({ color: 'light', colorVariant: 'L2' } as const) : undefined;
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={`${CLASSNAME}__wrapper`}
44
+ className={className}
43
45
  style={captionStyle}
44
- orientation="horizontal"
46
+ orientation="vertical"
45
47
  vAlign={align}
48
+ hAlign={align === 'center' ? align : undefined}
49
+ gap="regular"
46
50
  >
47
51
  {(title || description) && (
48
- <div className={`${CLASSNAME}__caption`}>
52
+ <Text as="p" truncate={truncate}>
49
53
  {title && (
50
- <Text as="span" className={`${CLASSNAME}__title`} {...titleColor}>
54
+ <Text as="span" typography="subtitle1" {...titleColor}>
51
55
  {title}
52
56
  </Text>
53
- )}
54
- {/* Add an `&nbsp;` when there is description and title. */}
55
- {title && description && '\u00A0'}
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} className={`${CLASSNAME}__tags`}>
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
- { image: IMAGES.portrait1s200, alt: 'Image 1' },
124
- { image: IMAGES.landscape1s200, alt: 'Image 2' },
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 { InheritedThumbnailProps } from '../types';
13
+ import type { ImageProps } from '../types';
13
14
 
14
- export interface ImageSlideProps extends InheritedThumbnailProps {
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 { isActive, scale, onScaleChange, image, imgRef: propImgRef, imgProps, alt } = props;
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, ZoomProps } from '../types';
10
+ import type { ImagesProps, InheritedSlideShowProps, ZoomButtonProps } from '../types';
10
11
  import { ImageSlide } from './ImageSlide';
11
12
 
12
- export interface ImageSlideshowProps extends InheritedSlideShowProps, ZoomProps, ImagesProps {
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, ...props }, index) => {
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
- imgRef={mergeRefs(imgRef, isActive ? activeImageRef : undefined)}
133
- image={image}
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 ZoomProps {
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<ThumbnailProps, 'image' | 'alt' | 'imgProps' | 'imgRef'>;
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 thumbnail to show on open */
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<LightboxProps, 'isOpen' | 'parentElement' | 'onClose' | 'closeButtonProps'>;
34
-
35
- export type InheritedAriaAttributes = Pick<React.AriaAttributes, 'aria-label' | 'aria-labelledby'>;
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
- ZoomProps,
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 images Images to display in the image lightbox
28
+ * @param initialProps Images to display in the image lightbox
27
29
  */
28
- export function useImageLightbox(images: ImageLightboxProps['images'] = []): {
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: (index?: number) => { onClick: React.MouseEventHandler; ref: React.Ref<any> };
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>(EMPTY_PROPS);
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 }) => ({ ...EMPTY_PROPS, 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, index?: number) {
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[index as any]?.current || findImage(triggerElement);
84
+ const triggerImage = triggerImageRefs[activeImageIndex as any]?.current || findImage(triggerElement);
75
85
 
76
- // Inject the trigger image size as a fallback for better loading state
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 === index && !image.imgProps?.width && !image.imgProps?.height) {
79
- const imgProps = {
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: index || 0,
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((index?: number) => ({
116
+ return memoize((options?: TriggerOptions) => ({
111
117
  ref(element: HTMLElement | null) {
112
- const triggerImage = findImage(element);
113
- if (index !== undefined && triggerImage) triggerImageRefs[index] = { current: triggerImage };
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, index);
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?.current?.getBoundingClientRect();
21
+ const newSize = elementRef.current?.getBoundingClientRect();
22
22
  if (newSize) setSize(newSize);
23
23
  }, 10),
24
24
  [elementRef],
@@ -0,0 +1,6 @@
1
+ /**
2
+ * File generated when storybook is started. Do not edit directly!
3
+ */
4
+ export default { title: 'LumX components/image-lightbox/ImageLightbox Demos' };
5
+
6
+ export { App as Default } from './default';
@@ -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);