@lumx/react 2.1.9 → 2.2.1

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 (68) hide show
  1. package/esm/_internal/Avatar2.js +7 -2
  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 +181 -782
  6. package/esm/_internal/Thumbnail2.js.map +1 -1
  7. package/esm/_internal/Tooltip2.js +0 -5
  8. package/esm/_internal/Tooltip2.js.map +1 -1
  9. package/esm/_internal/UserBlock.js +41 -17
  10. package/esm/_internal/UserBlock.js.map +1 -1
  11. package/esm/_internal/avatar.js +0 -3
  12. package/esm/_internal/avatar.js.map +1 -1
  13. package/esm/_internal/comment-block.js +0 -3
  14. package/esm/_internal/comment-block.js.map +1 -1
  15. package/esm/_internal/image-block.js +0 -3
  16. package/esm/_internal/image-block.js.map +1 -1
  17. package/esm/_internal/link-preview.js +0 -3
  18. package/esm/_internal/link-preview.js.map +1 -1
  19. package/esm/_internal/mdi.js +2 -2
  20. package/esm/_internal/mdi.js.map +1 -1
  21. package/esm/_internal/mosaic.js +0 -3
  22. package/esm/_internal/mosaic.js.map +1 -1
  23. package/esm/_internal/post-block.js +0 -3
  24. package/esm/_internal/post-block.js.map +1 -1
  25. package/esm/_internal/slider.js +1 -2
  26. package/esm/_internal/slider.js.map +1 -1
  27. package/esm/_internal/thumbnail.js +1 -4
  28. package/esm/_internal/thumbnail.js.map +1 -1
  29. package/esm/_internal/types.js +1 -0
  30. package/esm/_internal/types.js.map +1 -1
  31. package/esm/_internal/user-block.js +2 -3
  32. package/esm/_internal/user-block.js.map +1 -1
  33. package/esm/index.js +2 -4
  34. package/esm/index.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/components/avatar/Avatar.stories.tsx +30 -53
  37. package/src/components/avatar/Avatar.tsx +9 -0
  38. package/src/components/avatar/__snapshots__/Avatar.test.tsx.snap +220 -357
  39. package/src/components/image-block/__snapshots__/ImageBlock.test.tsx.snap +1 -1
  40. package/src/components/mosaic/__snapshots__/Mosaic.test.tsx.snap +30 -30
  41. package/src/components/post-block/__snapshots__/PostBlock.test.tsx.snap +1 -1
  42. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +10 -10
  43. package/src/components/table/__snapshots__/Table.test.tsx.snap +3 -3
  44. package/src/components/thumbnail/Thumbnail.stories.tsx +428 -52
  45. package/src/components/thumbnail/Thumbnail.test.tsx +8 -2
  46. package/src/components/thumbnail/Thumbnail.tsx +84 -47
  47. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +28 -81
  48. package/src/components/thumbnail/index.ts +1 -0
  49. package/src/components/thumbnail/useFocusPointStyle.tsx +89 -0
  50. package/src/components/thumbnail/useImageLoad.ts +24 -23
  51. package/src/components/tooltip/Tooltip.stories.tsx +7 -4
  52. package/src/components/tooltip/useInjectTooltipRef.tsx +1 -3
  53. package/src/components/user-block/UserBlock.stories.tsx +65 -105
  54. package/src/components/user-block/UserBlock.test.tsx +6 -0
  55. package/src/components/user-block/UserBlock.tsx +50 -25
  56. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +113 -144
  57. package/src/stories/generated/Badge/Demos.stories.tsx +1 -0
  58. package/src/stories/generated/Flag/Demos.stories.tsx +6 -0
  59. package/src/stories/generated/List/Demos.stories.tsx +2 -0
  60. package/src/stories/generated/Thumbnail/Demos.stories.tsx +1 -0
  61. package/src/stories/knobs/focusKnob.ts +1 -1
  62. package/src/stories/knobs/image.ts +35 -3
  63. package/src/stories/utils/CustomLink.tsx +7 -0
  64. package/types.d.ts +21 -4
  65. package/esm/_internal/clamp.js +0 -22
  66. package/esm/_internal/clamp.js.map +0 -1
  67. package/src/components/thumbnail/useClickable.ts +0 -26
  68. 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,104 @@ 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
+ wrapperProps.type = forwardedProps.type || 'button';
155
+ wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt;
156
+ }
137
157
 
138
158
  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
- >
159
+ <Wrapper
160
+ {...wrapperProps}
161
+ ref={ref}
162
+ className={classNames(
163
+ linkProps?.className,
164
+ className,
165
+ handleBasicClasses({
166
+ align,
167
+ aspectRatio,
168
+ prefix: CLASSNAME,
169
+ size,
170
+ theme,
171
+ variant,
172
+ isClickable,
173
+ hasError,
174
+ hasIconErrorFallback,
175
+ hasCustomErrorFallback,
176
+ isLoading,
177
+ hasBadge: !!badge,
178
+ }),
179
+ fillHeight && `${CLASSNAME}--fill-height`,
180
+ )}
181
+ >
182
+ <div className={`${CLASSNAME}__background`}>
150
183
  <img
151
184
  {...imgProps}
152
185
  style={{
153
186
  ...imgProps?.style,
154
- ...style?.image,
187
+ ...imageErrorStyle,
188
+ ...focusPointStyle,
155
189
  }}
156
- ref={mergeRefs(imgRef, propImgRef)}
157
- className={style?.image ? `${CLASSNAME}__focused-image` : `${CLASSNAME}__image`}
158
- crossOrigin={crossOrigin && !isInternetExplorer() ? crossOrigin : undefined}
190
+ ref={mergeRefs(setImgElement, propImgRef)}
191
+ className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}
192
+ crossOrigin={crossOrigin}
159
193
  src={image}
160
194
  alt={alt}
161
195
  loading={loading}
162
196
  />
197
+ {!isLoading && hasError && (
198
+ <div className={`${CLASSNAME}__fallback`}>
199
+ {hasIconErrorFallback ? (
200
+ <Icon icon={fallback as string} size={Size.xxs} theme={theme} />
201
+ ) : (
202
+ fallback
203
+ )}
204
+ </div>
205
+ )}
163
206
  </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
207
  {badge &&
171
208
  React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}
172
- </div>
209
+ </Wrapper>
173
210
  );
174
211
  });
175
212
  Thumbnail.displayName = COMPONENT_NAME;
@@ -1,123 +1,76 @@
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
+ aria-label="Click me"
6
+ 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
7
  onClick={[Function]}
7
- onKeyPress={[Function]}
8
- role="button"
9
- tabIndex={0}
8
+ type="button"
10
9
  >
11
10
  <div
12
11
  className="lumx-thumbnail__background"
13
- style={
14
- Object {
15
- "display": undefined,
16
- "visibility": "hidden",
17
- }
18
- }
19
12
  >
20
13
  <img
21
14
  alt="Click me"
22
- className="lumx-thumbnail__image"
15
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
23
16
  loading="lazy"
24
17
  src="/demo-assets/landscape1.jpg"
25
18
  style={Object {}}
26
19
  />
27
20
  </div>
28
- </div>
21
+ </button>
29
22
  `;
30
23
 
31
- exports[`<Thumbnail> Snapshots and structure should render story 'CustomFallback' 1`] = `
32
- <div
33
- className="lumx-thumbnail lumx-thumbnail--theme-light"
24
+ exports[`<Thumbnail> Snapshots and structure should render story 'ClickableCustomLink' 1`] = `
25
+ <CustomLink
26
+ 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"
27
+ href="https://google.fr"
34
28
  >
35
29
  <div
36
30
  className="lumx-thumbnail__background"
37
- style={
38
- Object {
39
- "display": undefined,
40
- "visibility": "hidden",
41
- }
42
- }
43
31
  >
44
32
  <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"
33
+ alt="Click me"
34
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
71
35
  loading="lazy"
72
36
  src="/demo-assets/landscape1.jpg"
73
37
  style={Object {}}
74
38
  />
75
39
  </div>
76
- </div>
40
+ </CustomLink>
77
41
  `;
78
42
 
79
- exports[`<Thumbnail> Snapshots and structure should render story 'DefaultFallback' 1`] = `
80
- <div
81
- className="lumx-thumbnail lumx-thumbnail--theme-light"
43
+ exports[`<Thumbnail> Snapshots and structure should render story 'ClickableLink' 1`] = `
44
+ <a
45
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
46
+ href="https://google.fr"
82
47
  >
83
48
  <div
84
49
  className="lumx-thumbnail__background"
85
- style={
86
- Object {
87
- "display": undefined,
88
- "visibility": "hidden",
89
- }
90
- }
91
50
  >
92
51
  <img
93
- alt="foo"
94
- className="lumx-thumbnail__image"
52
+ alt="Click me"
53
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
95
54
  loading="lazy"
96
- src="foo"
55
+ src="/demo-assets/landscape1.jpg"
97
56
  style={Object {}}
98
57
  />
99
58
  </div>
100
- </div>
59
+ </a>
101
60
  `;
102
61
 
103
- exports[`<Thumbnail> Snapshots and structure should render story 'IconFallback' 1`] = `
62
+ exports[`<Thumbnail> Snapshots and structure should render story 'Default' 1`] = `
104
63
  <div
105
- className="lumx-thumbnail lumx-thumbnail--theme-light"
64
+ className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--theme-light lumx-thumbnail--variant-squared lumx-thumbnail--is-loading"
106
65
  >
107
66
  <div
108
67
  className="lumx-thumbnail__background"
109
- style={
110
- Object {
111
- "display": undefined,
112
- "visibility": "hidden",
113
- }
114
- }
115
68
  >
116
69
  <img
117
- alt="foo"
118
- className="lumx-thumbnail__image"
70
+ alt="Image alt text"
71
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
119
72
  loading="lazy"
120
- src="foo"
73
+ src="/demo-assets/landscape1.jpg"
121
74
  style={Object {}}
122
75
  />
123
76
  </div>
@@ -126,20 +79,14 @@ exports[`<Thumbnail> Snapshots and structure should render story 'IconFallback'
126
79
 
127
80
  exports[`<Thumbnail> Snapshots and structure should render story 'WithBadge' 1`] = `
128
81
  <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"
82
+ 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
83
  >
131
84
  <div
132
85
  className="lumx-thumbnail__background"
133
- style={
134
- Object {
135
- "display": undefined,
136
- "visibility": "hidden",
137
- }
138
- }
139
86
  >
140
87
  <img
141
88
  alt="Image alt text"
142
- className="lumx-thumbnail__image"
89
+ className="lumx-thumbnail__image lumx-thumbnail__image--is-loading"
143
90
  loading="lazy"
144
91
  src="/demo-assets/landscape1.jpg"
145
92
  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
  }
@@ -76,13 +76,16 @@ export const EmptyTooltip = () => (
76
76
  );
77
77
 
78
78
  export const TooltipWithDropdown = () => {
79
- const buttonRef = useRef(null);
79
+ const [button, setButton] = useState<HTMLElement | null>(null);
80
+ const [isOpen, setOpen] = useState(false);
80
81
  return (
81
82
  <>
82
- <Tooltip label="Tooltip">
83
- <Button ref={buttonRef}>Anchor</Button>
83
+ <Tooltip label={!isOpen && 'Tooltip'} placement="top">
84
+ <Button ref={setButton} onClick={() => setOpen((o) => !o)}>
85
+ Anchor
86
+ </Button>
84
87
  </Tooltip>
85
- <Dropdown anchorRef={buttonRef} isOpen>
88
+ <Dropdown anchorRef={{ current: button }} isOpen={isOpen}>
86
89
  Dropdown
87
90
  </Dropdown>
88
91
  </>
@@ -28,9 +28,7 @@ export const useInjectTooltipRef = (
28
28
  get(children, 'props.isDisabled') !== true
29
29
  ) {
30
30
  const element = children as any;
31
- if (element.ref) {
32
- setAnchorElement(element.ref.current);
33
- }
31
+
34
32
  return cloneElement(element, {
35
33
  ...element.props,
36
34
  ...ariaProps,