@lumx/react 2.1.9 → 2.2.0

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.
Files changed (53) hide show
  1. package/esm/_internal/Avatar2.js +2 -1
  2. package/esm/_internal/Avatar2.js.map +1 -1
  3. package/esm/_internal/Slider2.js +21 -2
  4. package/esm/_internal/Slider2.js.map +1 -1
  5. package/esm/_internal/Thumbnail2.js +179 -782
  6. package/esm/_internal/Thumbnail2.js.map +1 -1
  7. package/esm/_internal/avatar.js +0 -3
  8. package/esm/_internal/avatar.js.map +1 -1
  9. package/esm/_internal/comment-block.js +0 -3
  10. package/esm/_internal/comment-block.js.map +1 -1
  11. package/esm/_internal/image-block.js +0 -3
  12. package/esm/_internal/image-block.js.map +1 -1
  13. package/esm/_internal/link-preview.js +0 -3
  14. package/esm/_internal/link-preview.js.map +1 -1
  15. package/esm/_internal/mdi.js +2 -2
  16. package/esm/_internal/mdi.js.map +1 -1
  17. package/esm/_internal/mosaic.js +0 -3
  18. package/esm/_internal/mosaic.js.map +1 -1
  19. package/esm/_internal/post-block.js +0 -3
  20. package/esm/_internal/post-block.js.map +1 -1
  21. package/esm/_internal/slider.js +1 -2
  22. package/esm/_internal/slider.js.map +1 -1
  23. package/esm/_internal/thumbnail.js +1 -4
  24. package/esm/_internal/thumbnail.js.map +1 -1
  25. package/esm/_internal/types.js +1 -0
  26. package/esm/_internal/types.js.map +1 -1
  27. package/esm/_internal/user-block.js +0 -3
  28. package/esm/_internal/user-block.js.map +1 -1
  29. package/esm/index.js +2 -4
  30. package/esm/index.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/components/avatar/Avatar.tsx +1 -0
  33. package/src/components/avatar/__snapshots__/Avatar.test.tsx.snap +30 -30
  34. package/src/components/image-block/__snapshots__/ImageBlock.test.tsx.snap +1 -1
  35. package/src/components/mosaic/__snapshots__/Mosaic.test.tsx.snap +30 -30
  36. package/src/components/post-block/__snapshots__/PostBlock.test.tsx.snap +1 -1
  37. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +10 -10
  38. package/src/components/table/__snapshots__/Table.test.tsx.snap +3 -3
  39. package/src/components/thumbnail/Thumbnail.stories.tsx +427 -52
  40. package/src/components/thumbnail/Thumbnail.test.tsx +14 -2
  41. package/src/components/thumbnail/Thumbnail.tsx +82 -47
  42. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +26 -81
  43. package/src/components/thumbnail/index.ts +1 -0
  44. package/src/components/thumbnail/useFocusPointStyle.tsx +89 -0
  45. package/src/components/thumbnail/useImageLoad.ts +24 -23
  46. package/src/stories/generated/List/Demos.stories.tsx +2 -0
  47. package/src/stories/knobs/focusKnob.ts +1 -1
  48. package/src/stories/knobs/image.ts +35 -3
  49. package/types.d.ts +7 -0
  50. package/esm/_internal/clamp.js +0 -22
  51. package/esm/_internal/clamp.js.map +0 -1
  52. package/src/components/thumbnail/useClickable.ts +0 -26
  53. package/src/components/thumbnail/useFocusPoint.ts +0 -154
@@ -1,4 +1,5 @@
1
1
  import React, {
2
+ CSSProperties,
2
3
  forwardRef,
3
4
  ImgHTMLAttributes,
4
5
  KeyboardEventHandler,
@@ -6,7 +7,6 @@ import React, {
6
7
  ReactElement,
7
8
  ReactNode,
8
9
  Ref,
9
- useRef,
10
10
  useState,
11
11
  } from 'react';
12
12
  import classNames from 'classnames';
@@ -15,12 +15,10 @@ import { AspectRatio, HorizontalAlignment, Icon, Size, Theme } from '@lumx/react
15
15
 
16
16
  import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
17
17
 
18
- import { mdiImageBrokenVariant } from '@lumx/icons';
19
- import { isInternetExplorer } from '@lumx/react/utils/isInternetExplorer';
18
+ import { mdiImageBroken } from '@lumx/icons';
20
19
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
21
- import { useFocusPoint } from '@lumx/react/components/thumbnail/useFocusPoint';
22
20
  import { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';
23
- import { useClickable } from '@lumx/react/components/thumbnail/useClickable';
21
+ import { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPointStyle';
24
22
  import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
25
23
 
26
24
  type ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;
@@ -51,6 +49,8 @@ export interface ThumbnailProps extends GenericProps {
51
49
  imgProps?: ImgHTMLProps;
52
50
  /** Reference to the native <img> element. */
53
51
  imgRef?: Ref<HTMLImageElement>;
52
+ /** Set to true to force the display of the loading skeleton. */
53
+ isLoading?: boolean;
54
54
  /** Size variant of the component. */
55
55
  size?: ThumbnailSize;
56
56
  /** Image loading mode. */
@@ -63,6 +63,10 @@ export interface ThumbnailProps extends GenericProps {
63
63
  theme?: Theme;
64
64
  /** Variant of the component. */
65
65
  variant?: ThumbnailVariant;
66
+ /** Props to pass to the link wrapping the thumbnail. */
67
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
68
+ /** Custom react component for the link (can be used to inject react router Link). */
69
+ linkAs?: 'a' | any;
66
70
  }
67
71
 
68
72
  /**
@@ -79,7 +83,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
79
83
  * Component default props.
80
84
  */
81
85
  const DEFAULT_PROPS: Partial<ThumbnailProps> = {
82
- fallback: mdiImageBrokenVariant,
86
+ fallback: mdiImageBroken,
83
87
  loading: 'lazy',
84
88
  theme: Theme.light,
85
89
  };
@@ -95,7 +99,7 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
95
99
  const {
96
100
  align,
97
101
  alt,
98
- aspectRatio,
102
+ aspectRatio = AspectRatio.original,
99
103
  badge,
100
104
  className,
101
105
  crossOrigin,
@@ -105,71 +109,102 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
105
109
  image,
106
110
  imgProps,
107
111
  imgRef: propImgRef,
112
+ isLoading: isLoadingProp,
108
113
  loading,
109
114
  size,
110
115
  theme,
111
116
  variant,
117
+ linkProps,
118
+ linkAs,
112
119
  ...forwardedProps
113
120
  } = props;
114
- const imgRef = useRef<HTMLImageElement>(null);
121
+ const [imgElement, setImgElement] = useState<HTMLImageElement>();
115
122
 
116
123
  // Image loading state.
117
- const loadingState = useImageLoad(imgRef);
124
+ const loadingState = useImageLoad(image, imgElement);
125
+ const isLoaded = loadingState === 'isLoaded';
126
+ const isLoading = isLoadingProp || loadingState === 'isLoading';
118
127
  const hasError = loadingState === 'hasError';
119
- const isLoading = loadingState === 'isLoading';
120
128
 
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
- };
129
+ // Focus point.
130
+ const focusPointStyle = useFocusPointStyle(props, imgElement, isLoaded);
134
131
 
135
- // Update img style according to focus point and aspect ratio.
136
- const style = useFocusPoint({ image, focusPoint, aspectRatio, imgRef, loadingState, wrapper });
132
+ const hasIconErrorFallback = hasError && typeof fallback === 'string';
133
+ const hasCustomErrorFallback = hasError && !hasIconErrorFallback;
134
+ const imageErrorStyle: CSSProperties = {};
135
+ if (hasIconErrorFallback) {
136
+ // Keep the image layout on icon fallback.
137
+ imageErrorStyle.visibility = 'hidden';
138
+ } else if (hasCustomErrorFallback) {
139
+ // Remove the image on custom fallback.
140
+ imageErrorStyle.display = 'none';
141
+ }
142
+
143
+ const isLink = Boolean(linkProps?.href || linkAs);
144
+ const isButton = !!forwardedProps.onClick;
145
+ const isClickable = isButton || isLink;
146
+
147
+ let Wrapper: any = 'div';
148
+ const wrapperProps = { ...forwardedProps };
149
+ if (isLink) {
150
+ Wrapper = linkAs || 'a';
151
+ Object.assign(wrapperProps, linkProps);
152
+ } else if (isButton) {
153
+ Wrapper = 'button';
154
+ }
137
155
 
138
156
  return (
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
- >
157
+ <Wrapper
158
+ {...wrapperProps}
159
+ ref={ref}
160
+ className={classNames(
161
+ linkProps?.className,
162
+ className,
163
+ handleBasicClasses({
164
+ align,
165
+ aspectRatio,
166
+ prefix: CLASSNAME,
167
+ size,
168
+ theme,
169
+ variant,
170
+ isClickable,
171
+ hasError,
172
+ hasIconErrorFallback,
173
+ hasCustomErrorFallback,
174
+ isLoading,
175
+ hasBadge: !!badge,
176
+ }),
177
+ fillHeight && `${CLASSNAME}--fill-height`,
178
+ )}
179
+ >
180
+ <div className={`${CLASSNAME}__background`}>
150
181
  <img
151
182
  {...imgProps}
152
183
  style={{
153
184
  ...imgProps?.style,
154
- ...style?.image,
185
+ ...imageErrorStyle,
186
+ ...focusPointStyle,
155
187
  }}
156
- ref={mergeRefs(imgRef, propImgRef)}
157
- className={style?.image ? `${CLASSNAME}__focused-image` : `${CLASSNAME}__image`}
158
- crossOrigin={crossOrigin && !isInternetExplorer() ? crossOrigin : undefined}
188
+ ref={mergeRefs(setImgElement, propImgRef)}
189
+ className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}
190
+ crossOrigin={crossOrigin}
159
191
  src={image}
160
192
  alt={alt}
161
193
  loading={loading}
162
194
  />
195
+ {!isLoading && hasError && (
196
+ <div className={`${CLASSNAME}__fallback`}>
197
+ {hasIconErrorFallback ? (
198
+ <Icon icon={fallback as string} size={Size.xxs} theme={theme} />
199
+ ) : (
200
+ fallback
201
+ )}
202
+ </div>
203
+ )}
163
204
  </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
- ))}
170
205
  {badge &&
171
206
  React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}
172
- </div>
207
+ </Wrapper>
173
208
  );
174
209
  });
175
210
  Thumbnail.displayName = COMPONENT_NAME;
@@ -1,123 +1,74 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`<Thumbnail> Snapshots and structure should render story 'Clickable' 1`] = `
4
- <div
5
- className="lumx-thumbnail lumx-thumbnail--size-xxl lumx-thumbnail--theme-light"
4
+ <button
5
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
6
6
  onClick={[Function]}
7
- onKeyPress={[Function]}
8
- role="button"
9
- tabIndex={0}
10
7
  >
11
8
  <div
12
9
  className="lumx-thumbnail__background"
13
- style={
14
- Object {
15
- "display": undefined,
16
- "visibility": "hidden",
17
- }
18
- }
19
10
  >
20
11
  <img
21
12
  alt="Click me"
22
- className="lumx-thumbnail__image"
13
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
23
14
  loading="lazy"
24
15
  src="/demo-assets/landscape1.jpg"
25
16
  style={Object {}}
26
17
  />
27
18
  </div>
28
- </div>
19
+ </button>
29
20
  `;
30
21
 
31
- exports[`<Thumbnail> Snapshots and structure should render story 'CustomFallback' 1`] = `
32
- <div
33
- className="lumx-thumbnail lumx-thumbnail--theme-light"
22
+ exports[`<Thumbnail> Snapshots and structure should render story 'ClickableCustomLink' 1`] = `
23
+ <CustomLinkComponent
24
+ className="custom-class-name lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
25
+ href="https://google.fr"
34
26
  >
35
27
  <div
36
28
  className="lumx-thumbnail__background"
37
- style={
38
- Object {
39
- "display": undefined,
40
- "visibility": "hidden",
41
- }
42
- }
43
29
  >
44
30
  <img
45
- alt="foo"
46
- className="lumx-thumbnail__image"
47
- loading="lazy"
48
- src="foo"
49
- style={Object {}}
50
- />
51
- </div>
52
- </div>
53
- `;
54
-
55
- exports[`<Thumbnail> Snapshots and structure should render story 'Default' 1`] = `
56
- <div
57
- className="lumx-thumbnail lumx-thumbnail--size-xxl lumx-thumbnail--theme-light"
58
- >
59
- <div
60
- className="lumx-thumbnail__background"
61
- style={
62
- Object {
63
- "display": undefined,
64
- "visibility": "hidden",
65
- }
66
- }
67
- >
68
- <img
69
- alt="Image alt text"
70
- className="lumx-thumbnail__image"
31
+ alt="Click me"
32
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
71
33
  loading="lazy"
72
34
  src="/demo-assets/landscape1.jpg"
73
35
  style={Object {}}
74
36
  />
75
37
  </div>
76
- </div>
38
+ </CustomLinkComponent>
77
39
  `;
78
40
 
79
- exports[`<Thumbnail> Snapshots and structure should render story 'DefaultFallback' 1`] = `
80
- <div
81
- className="lumx-thumbnail lumx-thumbnail--theme-light"
41
+ exports[`<Thumbnail> Snapshots and structure should render story 'ClickableLink' 1`] = `
42
+ <a
43
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
44
+ href="https://google.fr"
82
45
  >
83
46
  <div
84
47
  className="lumx-thumbnail__background"
85
- style={
86
- Object {
87
- "display": undefined,
88
- "visibility": "hidden",
89
- }
90
- }
91
48
  >
92
49
  <img
93
- alt="foo"
94
- className="lumx-thumbnail__image"
50
+ alt="Click me"
51
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
95
52
  loading="lazy"
96
- src="foo"
53
+ src="/demo-assets/landscape1.jpg"
97
54
  style={Object {}}
98
55
  />
99
56
  </div>
100
- </div>
57
+ </a>
101
58
  `;
102
59
 
103
- exports[`<Thumbnail> Snapshots and structure should render story 'IconFallback' 1`] = `
60
+ exports[`<Thumbnail> Snapshots and structure should render story 'Default' 1`] = `
104
61
  <div
105
- className="lumx-thumbnail lumx-thumbnail--theme-light"
62
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--theme-light lumx-thumbnail--variant-squared lumx-thumbnail--is-loading"
106
63
  >
107
64
  <div
108
65
  className="lumx-thumbnail__background"
109
- style={
110
- Object {
111
- "display": undefined,
112
- "visibility": "hidden",
113
- }
114
- }
115
66
  >
116
67
  <img
117
- alt="foo"
118
- className="lumx-thumbnail__image"
68
+ alt="Image alt text"
69
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
119
70
  loading="lazy"
120
- src="foo"
71
+ src="/demo-assets/landscape1.jpg"
121
72
  style={Object {}}
122
73
  />
123
74
  </div>
@@ -126,20 +77,14 @@ exports[`<Thumbnail> Snapshots and structure should render story 'IconFallback'
126
77
 
127
78
  exports[`<Thumbnail> Snapshots and structure should render story 'WithBadge' 1`] = `
128
79
  <div
129
- className="lumx-thumbnail lumx-thumbnail--aspect-ratio-square lumx-thumbnail--size-l lumx-thumbnail--theme-light lumx-thumbnail--variant-rounded lumx-thumbnail--has-badge"
80
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-square lumx-thumbnail--size-l lumx-thumbnail--theme-light lumx-thumbnail--variant-rounded lumx-thumbnail--is-loading lumx-thumbnail--has-badge"
130
81
  >
131
82
  <div
132
83
  className="lumx-thumbnail__background"
133
- style={
134
- Object {
135
- "display": undefined,
136
- "visibility": "hidden",
137
- }
138
- }
139
84
  >
140
85
  <img
141
86
  alt="Image alt text"
142
- className="lumx-thumbnail__image"
87
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
143
88
  loading="lazy"
144
89
  src="/demo-assets/landscape1.jpg"
145
90
  style={Object {}}
@@ -1,2 +1,3 @@
1
1
  export * from './Thumbnail';
2
2
  export * from './types';
3
+ export { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPointStyle';
@@ -0,0 +1,89 @@
1
+ import { CSSProperties, useEffect, useMemo, useState } from 'react';
2
+ import { AspectRatio } from '@lumx/react/components';
3
+ import { ThumbnailProps } from '@lumx/react/components/thumbnail/Thumbnail';
4
+
5
+ // Calculate shift to center the focus point in the container.
6
+ function shiftPosition(scale: number, focusPoint: number, imageSize: number, containerSize: number) {
7
+ const scaledSize = imageSize / scale;
8
+ const scaledFocusHeight = focusPoint * scaledSize;
9
+ const startFocus = scaledFocusHeight - containerSize / 2;
10
+ const shift = startFocus / (scaledSize - containerSize);
11
+ return Math.floor(Math.max(Math.min(shift, 1), 0) * 100);
12
+ }
13
+
14
+ type Size = { width: number; height: number };
15
+
16
+ // Compute CSS properties to apply the focus point.
17
+ export const useFocusPointStyle = (
18
+ { image, aspectRatio, focusPoint, imgProps: { width, height } = {} }: ThumbnailProps,
19
+ element: HTMLImageElement | undefined,
20
+ isLoaded: boolean,
21
+ ): CSSProperties => {
22
+ // Get natural image size from imgProps or img element.
23
+ const imageSize: Size | undefined = useMemo(() => {
24
+ // Focus point is not applicable => exit early
25
+ if (!image || aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return undefined;
26
+ if (typeof width === 'number' && typeof height === 'number') return { width, height };
27
+ if (element && isLoaded) return { width: element.naturalWidth, height: element.naturalHeight };
28
+ return undefined;
29
+ }, [aspectRatio, element, focusPoint?.x, focusPoint?.y, height, image, isLoaded, width]);
30
+
31
+ // Get container size (dependant on imageSize).
32
+ const [containerSize, setContainerSize] = useState<Size | undefined>(undefined);
33
+ useEffect(
34
+ function updateContainerSize() {
35
+ const cWidth = element?.offsetWidth;
36
+ const cHeight = element?.offsetHeight;
37
+ if (cWidth && cHeight) {
38
+ // Update only if needed.
39
+ setContainerSize((oldContainerSize) =>
40
+ oldContainerSize?.width === cWidth && oldContainerSize.height === cHeight
41
+ ? oldContainerSize
42
+ : { width: cWidth, height: cHeight },
43
+ );
44
+ } else if (imageSize) {
45
+ // Wait for a render (in case the container size is dependent on the image size).
46
+ requestAnimationFrame(updateContainerSize);
47
+ }
48
+ },
49
+ [element?.offsetHeight, element?.offsetWidth, imageSize],
50
+ );
51
+
52
+ // Compute style.
53
+ const [style, setStyle] = useState<CSSProperties>({});
54
+ useEffect(() => {
55
+ // Focus point is not applicable => exit early
56
+ if (!image || aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) {
57
+ return;
58
+ }
59
+ if (!element || !imageSize) {
60
+ // Focus point can be computed but now right now (image size unknown).
61
+ setStyle({ visibility: 'hidden' });
62
+ return;
63
+ }
64
+ if (!containerSize) {
65
+ // Missing container size abort focus point compute.
66
+ setStyle({});
67
+ return;
68
+ }
69
+
70
+ const heightScale = imageSize.height / containerSize.height;
71
+ const widthScale = imageSize.width / containerSize.width;
72
+ const scale = Math.min(widthScale, heightScale);
73
+
74
+ // Focus Y relative to the top (instead of the center)
75
+ const focusPointFromTop = Math.abs((focusPoint?.y || 0) - 1) / 2;
76
+ const y = shiftPosition(scale, focusPointFromTop, imageSize.height, containerSize.height);
77
+
78
+ // Focus X relative to the left (instead of the center)
79
+ const focusPointFromLeft = Math.abs((focusPoint?.x || 0) + 1) / 2;
80
+ const x = shiftPosition(scale, focusPointFromLeft, imageSize.width, containerSize.width);
81
+
82
+ const objectPosition = `${x}% ${y}%`;
83
+
84
+ // Update only if needed.
85
+ setStyle((oldStyle) => (oldStyle.objectPosition === objectPosition ? oldStyle : { objectPosition }));
86
+ }, [aspectRatio, containerSize, element, focusPoint?.x, focusPoint?.y, image, imageSize]);
87
+
88
+ return style;
89
+ };
@@ -1,39 +1,40 @@
1
- import { RefObject, useCallback, useEffect, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
3
  export type LoadingState = 'isLoading' | 'isLoaded' | 'hasError';
4
4
 
5
- export function useImageLoad(imgRef?: RefObject<HTMLImageElement>): LoadingState {
6
- const [state, setState] = useState<LoadingState>('isLoading');
7
-
8
- const update = useCallback(
9
- (event?: any) => {
10
- const img = imgRef?.current;
11
- if (!img || !img.complete) {
12
- setState('isLoading');
13
- return;
14
- }
5
+ function getState(img: HTMLImageElement | null | undefined, event?: Event) {
6
+ // Error event occurred or image loaded empty.
7
+ if (event?.type === 'error' || (img?.complete && (img?.naturalWidth === 0 || img?.naturalHeight === 0))) {
8
+ return 'hasError';
9
+ }
10
+ // Image is undefined or incomplete.
11
+ if (!img || !img.complete) {
12
+ return 'isLoading';
13
+ }
14
+ // Else loaded.
15
+ return 'isLoaded';
16
+ }
15
17
 
16
- if (event?.type === 'error' || (img.complete && img?.naturalWidth === 0)) {
17
- setState('hasError');
18
- return;
19
- }
18
+ export function useImageLoad(imageURL: string, imgRef?: HTMLImageElement): LoadingState {
19
+ const [state, setState] = useState<LoadingState>(getState(imgRef));
20
20
 
21
- setState('isLoaded');
22
- },
23
- [imgRef],
24
- );
21
+ // Update state when changing image URL or DOM reference.
22
+ useEffect(() => {
23
+ setState(getState(imgRef));
24
+ }, [imageURL, imgRef]);
25
25
 
26
+ // Listen to `load` and `error` event on image
26
27
  useEffect(() => {
27
- const img = imgRef?.current;
28
+ const img = imgRef;
28
29
  if (!img) return undefined;
29
-
30
- update();
30
+ const update = (event?: Event) => setState(getState(img, event));
31
31
  img.addEventListener('load', update);
32
32
  img.addEventListener('error', update);
33
33
  return () => {
34
34
  img.removeEventListener('load', update);
35
35
  img.removeEventListener('error', update);
36
36
  };
37
- }, [update, imgRef, imgRef?.current?.src]);
37
+ }, [imgRef, imgRef?.src]);
38
+
38
39
  return state;
39
40
  }
@@ -4,6 +4,8 @@
4
4
  export default { title: 'LumX components/list/List Demos' };
5
5
 
6
6
  export { App as Big } from './big';
7
+ export { App as Clickable } from './clickable';
7
8
  export { App as Huge } from './huge';
9
+ export { App as Paddings } from './paddings';
8
10
  export { App as Regular } from './regular';
9
11
  export { App as Tiny } from './tiny';
@@ -1,3 +1,3 @@
1
1
  import { number } from '@storybook/addon-knobs';
2
2
 
3
- export const focusKnob = (name: string) => number(name, 0, { max: 1, min: -1, range: true, step: 0.01 });
3
+ export const focusKnob = (name: string, value = 0) => number(name, value, { max: 1, min: -1, range: true, step: 0.01 });
@@ -6,18 +6,19 @@ const avatar3 = '/demo-assets/avatar3.jpg';
6
6
  const avatar4 = '/demo-assets/avatar4.jpg';
7
7
  const landscape1 = '/demo-assets/landscape1.jpg';
8
8
  const landscape2 = '/demo-assets/landscape2.jpg';
9
+ const landscape1s200 = '/demo-assets/landscape1-s200.jpg';
9
10
  const landscape3 = '/demo-assets/landscape3.jpg';
10
11
  const portrait1 = '/demo-assets/portrait1.jpg';
12
+ const portrait1s200 = '/demo-assets/portrait1-s200.jpg';
11
13
  const portrait2 = '/demo-assets/portrait2.jpg';
12
14
  const portrait3 = '/demo-assets/portrait3.jpg';
13
15
  const square1 = '/demo-assets/square1.jpg';
14
16
  const square2 = '/demo-assets/square2.jpg';
15
17
 
16
18
  export const AVATAR_IMAGES = { avatar1, avatar2, avatar3, avatar4 };
17
-
18
19
  export const SQUARE_IMAGES = { square1, square2 };
19
- export const LANDSCAPE_IMAGES = { landscape1, landscape2, landscape3 };
20
- export const PORTRAIT_IMAGES = { portrait1, portrait2, portrait3 };
20
+ export const LANDSCAPE_IMAGES = { landscape1, landscape1s200, landscape2, landscape3 };
21
+ export const PORTRAIT_IMAGES = { portrait1, portrait1s200, portrait2, portrait3 };
21
22
 
22
23
  export const IMAGES = { ...LANDSCAPE_IMAGES, ...PORTRAIT_IMAGES, ...SQUARE_IMAGES, ...AVATAR_IMAGES };
23
24
 
@@ -35,3 +36,34 @@ export const squareImageKnob = (name = 'Image', value = Object.values(SQUARE_IMA
35
36
 
36
37
  export const imageKnob = (name = 'Image', value = Object.values(IMAGES)[0], groupId?: string) =>
37
38
  select(name, IMAGES, value, groupId);
39
+
40
+ type Size = { width: number; height: number };
41
+
42
+ export const AVATAR_IMAGE_SIZES: Record<keyof typeof AVATAR_IMAGES, Size> = {
43
+ avatar1: { width: 128, height: 128 },
44
+ avatar2: { width: 150, height: 150 },
45
+ avatar3: { width: 128, height: 128 },
46
+ avatar4: { width: 128, height: 128 },
47
+ };
48
+ export const SQUARE_IMAGE_SIZES: Record<keyof typeof SQUARE_IMAGES, Size> = {
49
+ square1: { width: 72, height: 72 },
50
+ square2: { width: 300, height: 300 },
51
+ };
52
+ export const LANDSCAPE_IMAGE_SIZES: Record<keyof typeof LANDSCAPE_IMAGES, Size> = {
53
+ landscape1: { width: 800, height: 546 },
54
+ landscape1s200: { width: 200, height: 150 },
55
+ landscape2: { width: 800, height: 600 },
56
+ landscape3: { width: 640, height: 480 },
57
+ };
58
+ export const PORTRAIT_IMAGE_SIZES: Record<keyof typeof PORTRAIT_IMAGES, Size> = {
59
+ portrait1: { width: 275, height: 500 },
60
+ portrait1s200: { width: 200, height: 364 },
61
+ portrait2: { width: 350, height: 500 },
62
+ portrait3: { width: 300, height: 500 },
63
+ };
64
+ export const IMAGE_SIZES: Record<keyof typeof IMAGES, Size> = {
65
+ ...LANDSCAPE_IMAGE_SIZES,
66
+ ...PORTRAIT_IMAGE_SIZES,
67
+ ...SQUARE_IMAGE_SIZES,
68
+ ...AVATAR_IMAGE_SIZES,
69
+ };
package/types.d.ts CHANGED
@@ -1233,6 +1233,8 @@ export interface ThumbnailProps extends GenericProps {
1233
1233
  imgProps?: ImgHTMLProps;
1234
1234
  /** Reference to the native <img> element. */
1235
1235
  imgRef?: Ref<HTMLImageElement>;
1236
+ /** Set to true to force the display of the loading skeleton. */
1237
+ isLoading?: boolean;
1236
1238
  /** Size variant of the component. */
1237
1239
  size?: ThumbnailSize;
1238
1240
  /** Image loading mode. */
@@ -1245,6 +1247,10 @@ export interface ThumbnailProps extends GenericProps {
1245
1247
  theme?: Theme;
1246
1248
  /** Variant of the component. */
1247
1249
  variant?: ThumbnailVariant;
1250
+ /** Props to pass to the link wrapping the thumbnail. */
1251
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
1252
+ /** Custom react component for the link (can be used to inject react router Link). */
1253
+ linkAs?: "a" | any;
1248
1254
  }
1249
1255
  /**
1250
1256
  * Thumbnail component.
@@ -2427,6 +2433,7 @@ export interface TextFieldProps extends GenericProps {
2427
2433
  * @return React element.
2428
2434
  */
2429
2435
  export declare const TextField: Comp<TextFieldProps, HTMLDivElement>;
2436
+ export declare const useFocusPointStyle: ({ image, aspectRatio, focusPoint, imgProps: { width, height } }: ThumbnailProps, element: HTMLImageElement | undefined, isLoaded: boolean) => CSSProperties;
2430
2437
  /**
2431
2438
  * Defines the props of the component.
2432
2439
  */
@@ -1,22 +0,0 @@
1
- /**
2
- * Clamp value in range.
3
- *
4
- * @param value Value to clamp.
5
- * @param min Minimum value.
6
- * @param max Maximum value.
7
- * @return Clamped value.
8
- */
9
- var clamp = function clamp(value, min, max) {
10
- if (value < min) {
11
- return min;
12
- }
13
-
14
- if (value > max) {
15
- return max;
16
- }
17
-
18
- return value;
19
- };
20
-
21
- export { clamp as c };
22
- //# sourceMappingURL=clamp.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"clamp.js","sources":["../../../src/utils/clamp.ts"],"sourcesContent":["/**\n * Clamp value in range.\n *\n * @param value Value to clamp.\n * @param min Minimum value.\n * @param max Maximum value.\n * @return Clamped value.\n */\nexport const clamp = (value: number, min: number, max: number): number => {\n if (value < min) {\n return min;\n }\n if (value > max) {\n return max;\n }\n return value;\n};\n"],"names":["clamp","value","min","max"],"mappings":"AAAA;;;;;;;;IAQaA,KAAK,GAAG,SAARA,KAAQ,CAACC,KAAD,EAAgBC,GAAhB,EAA6BC,GAA7B,EAAqD;AACtE,MAAIF,KAAK,GAAGC,GAAZ,EAAiB;AACb,WAAOA,GAAP;AACH;;AACD,MAAID,KAAK,GAAGE,GAAZ,EAAiB;AACb,WAAOA,GAAP;AACH;;AACD,SAAOF,KAAP;AACH;;;;"}