@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/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",
11
- "@lumx/icons": "^3.7.6-alpha.11",
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.11"
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 { toNestedProps } from '@lumx/react/stories/decorators/withNestedProps';
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 Default = (props: any) => {
34
- const nestedProps = toNestedProps(props) as any;
35
- return <ImageBlock {...nestedProps} />;
36
- };
37
-
38
- export const WithCaptionBelow: any = Default.bind({});
39
- WithCaptionBelow.args = {
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: any = Default.bind({});
47
- WithCaptionOver.args = {
48
- captionPosition: ImageBlockCaptionPosition.over,
49
- size: Size.xxl,
50
- title: 'Image block title',
51
- description: 'Image block description',
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: any = Default.bind({});
55
- WithAlign.args = {
56
- ...WithCaptionBelow.args,
57
- image: LANDSCAPE_IMAGES.landscape1s200,
58
- size: undefined,
59
- align: Alignment.center,
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: any = Default.bind({});
63
- WithTags.argTypes = {
64
- tags: { control: false },
65
- };
66
- WithTags.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
- ),
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: any = Default.bind({});
77
- WithActions.argTypes = {
78
- actions: { control: false },
79
- };
80
- WithActions.args = {
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: any = Default.bind({});
86
- WithFocusPointHorizontal.args = {
87
- size: Size.xxl,
88
- 'thumbnailProps.aspectRatio': AspectRatio.vertical,
89
- 'thumbnailProps.focusPoint.x': 1,
90
- 'thumbnailProps.focusPoint.y': 0,
91
- };
92
- WithFocusPointHorizontal.argTypes = {
93
- 'thumbnailProps.focusPoint.x': focusPoint,
94
- 'thumbnailProps.focusPoint.y': focusPoint,
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: any = Default.bind({});
98
- WithFocusPointVertical.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,
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
- WithFocusPointVertical.argTypes = {
106
- 'thumbnailProps.focusPoint.x': focusPoint,
107
- 'thumbnailProps.focusPoint.y': focusPoint,
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
  };
@@ -117,6 +117,7 @@ export const ImageBlock: Comp<ImageBlockProps, HTMLDivElement> = forwardRef((pro
117
117
  alt={(alt || title) as string}
118
118
  />
119
119
  <ImageCaption
120
+ as="figcaption"
120
121
  className={`${CLASSNAME}__wrapper`}
121
122
  theme={theme}
122
123
  title={title}
@@ -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?: boolean;
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 descriptionColor = { color: theme === 'dark' ? 'light' : 'dark', colorVariant: 'L2' } as 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" {...descriptionColor} {...descriptionContent} />}
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) => queryByClassName(imageLightbox, 'lumx-image-lightbox__image-slide'),
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 initialWidth = Number.parseInt(imgProps?.width as any, 10);
43
- const initialHeight = Number.parseInt(imgProps?.height as any, 10);
44
- return initialWidth && initialHeight ? { width: initialWidth, height: initialHeight } : null;
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: P & ManagedProps;
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<P & ManagedProps>(basePropsRef.current);
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 closeLightbox() {
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 with reset empty props
71
- setImageLightboxProps(({ parentElement }) => ({ ...basePropsRef.current, parentElement }));
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 openLightbox(triggerElement: HTMLElement, { activeImageIndex }: TriggerOptions = {}) {
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
- ...basePropsRef.current,
94
+ setImageLightboxProps((prevProps) => ({
95
+ ...prevProps,
99
96
  activeImageRef: currentImageRef,
100
97
  parentElement: { current: triggerElement },
101
98
  isOpen: true,
102
- onClose: closeLightbox,
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
- openLightbox(e.target as HTMLElement, options);
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,