@lumx/react 2.1.9-alpha-thumbnail12 → 2.1.9-alpha-thumbnail13

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.
@@ -1,6 +1,6 @@
1
1
  import { d as _slicedToArray, b as _objectWithoutProperties, _ as _objectSpread2, c as _extends } from './_rollupPluginBabelHelpers.js';
2
- import { Size, Theme, AspectRatio } from './components.js';
3
- import React, { useState, useEffect, forwardRef, useRef } from 'react';
2
+ import { AspectRatio, Size, Theme } from './components.js';
3
+ import React, { useState, useEffect, useMemo, forwardRef } from 'react';
4
4
  import { g as getRootClassName, c as classnames, h as handleBasicClasses } from './getRootClassName.js';
5
5
  import { r as mdiImageBroken } from './mdi.js';
6
6
  import { m as mergeRefs } from './mergeRefs.js';
@@ -22,20 +22,18 @@ function getState(img, event) {
22
22
  }
23
23
 
24
24
  function useImageLoad(imageURL, imgRef) {
25
- var _imgRef$current;
26
-
27
- var _useState = useState(getState(imgRef === null || imgRef === void 0 ? void 0 : imgRef.current)),
25
+ var _useState = useState(getState(imgRef)),
28
26
  _useState2 = _slicedToArray(_useState, 2),
29
27
  state = _useState2[0],
30
28
  setState = _useState2[1]; // Update state when changing image URL or DOM reference.
31
29
 
32
30
 
33
31
  useEffect(function () {
34
- setState(getState(imgRef === null || imgRef === void 0 ? void 0 : imgRef.current));
32
+ setState(getState(imgRef));
35
33
  }, [imageURL, imgRef]); // Listen to `load` and `error` event on image
36
34
 
37
35
  useEffect(function () {
38
- var img = imgRef === null || imgRef === void 0 ? void 0 : imgRef.current;
36
+ var img = imgRef;
39
37
  if (!img) return undefined;
40
38
 
41
39
  var update = function update(event) {
@@ -48,10 +46,68 @@ function useImageLoad(imageURL, imgRef) {
48
46
  img.removeEventListener('load', update);
49
47
  img.removeEventListener('error', update);
50
48
  };
51
- }, [imgRef, imgRef === null || imgRef === void 0 ? void 0 : (_imgRef$current = imgRef.current) === null || _imgRef$current === void 0 ? void 0 : _imgRef$current.src]);
49
+ }, [imgRef, imgRef === null || imgRef === void 0 ? void 0 : imgRef.src]);
52
50
  return state;
53
51
  }
54
52
 
53
+ function shiftPosition(scale, containerSize, imageSize, focusSize, isVertical) {
54
+ if (!focusSize) return 50;
55
+ var focusFactor = (focusSize + 1) / 2;
56
+ var scaledSize = Math.floor(imageSize / scale);
57
+ var focus = Math.floor(focusFactor * scaledSize);
58
+ if (isVertical) focus = scaledSize - focus;
59
+ var containerCenter = Math.floor(containerSize / 2);
60
+ var focusOffset = focus - containerCenter;
61
+ var remainder = scaledSize - focus;
62
+ if (remainder < containerCenter) focusOffset -= containerCenter - remainder;
63
+ if (focusOffset < 0) return 0;
64
+ return Math.min(100, Math.floor(focusOffset * 100 / containerSize));
65
+ }
66
+
67
+ var useFocusPointStyle = function useFocusPointStyle(_ref, element, isLoaded) {
68
+ var image = _ref.image,
69
+ aspectRatio = _ref.aspectRatio,
70
+ focusPoint = _ref.focusPoint,
71
+ _ref$imgProps = _ref.imgProps;
72
+ _ref$imgProps = _ref$imgProps === void 0 ? {} : _ref$imgProps;
73
+ var width = _ref$imgProps.width,
74
+ height = _ref$imgProps.height;
75
+ // Get natural image size from imgProps or img element.
76
+ var naturalSize = useMemo(function () {
77
+ if (!image || aspectRatio === AspectRatio.original || !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x) && !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y)) return undefined;
78
+ if (typeof width === 'number' && typeof height === 'number') return {
79
+ width: width,
80
+ height: height
81
+ };
82
+ if (element && isLoaded) return {
83
+ width: element.naturalWidth,
84
+ height: element.naturalHeight
85
+ };
86
+ return undefined;
87
+ }, [aspectRatio, element, focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x, focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y, height, image, isLoaded, width]); // Compute focus point CSS style.
88
+
89
+ return useMemo(function () {
90
+ if (aspectRatio === AspectRatio.original || !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x) && !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y)) return {};
91
+
92
+ if (element && naturalSize) {
93
+ var actualWidth = element.offsetWidth;
94
+ var actualHeight = element.offsetHeight;
95
+ var heightScale = actualHeight / naturalSize.height;
96
+ var widthScale = actualWidth / naturalSize.width;
97
+ var x = shiftPosition(heightScale, actualWidth, naturalSize.width, focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x);
98
+ var y = shiftPosition(widthScale, actualHeight, naturalSize.height, focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y, true);
99
+ return {
100
+ objectPosition: "".concat(x, "% ").concat(y, "%")
101
+ };
102
+ } // Focus point can't be computed yet => We hide the image until it can.
103
+
104
+
105
+ return {
106
+ visibility: 'hidden'
107
+ };
108
+ }, [aspectRatio, element, focusPoint, naturalSize]);
109
+ };
110
+
55
111
  /**
56
112
  * Component display name.
57
113
  */
@@ -70,13 +126,6 @@ var DEFAULT_PROPS = {
70
126
  loading: 'lazy',
71
127
  theme: Theme.light
72
128
  };
73
-
74
- function getObjectPosition(aspectRatio, focusPoint) {
75
- if (aspectRatio === AspectRatio.original || !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y) && !(focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x)) return undefined;
76
- var x = Math.round(Math.abs((((focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.x) || 0) + 1) / 2) * 100);
77
- var y = Math.round(Math.abs((((focusPoint === null || focusPoint === void 0 ? void 0 : focusPoint.y) || 0) - 1) / 2) * 100);
78
- return "".concat(x, "% ").concat(y, "%");
79
- }
80
129
  /**
81
130
  * Thumbnail component.
82
131
  *
@@ -85,7 +134,6 @@ function getObjectPosition(aspectRatio, focusPoint) {
85
134
  * @return React element.
86
135
  */
87
136
 
88
-
89
137
  var Thumbnail = forwardRef(function (props, ref) {
90
138
  var align = props.align,
91
139
  alt = props.alt,
@@ -109,11 +157,18 @@ var Thumbnail = forwardRef(function (props, ref) {
109
157
  linkAs = props.linkAs,
110
158
  forwardedProps = _objectWithoutProperties(props, ["align", "alt", "aspectRatio", "badge", "className", "crossOrigin", "fallback", "fillHeight", "focusPoint", "image", "imgProps", "imgRef", "isLoading", "loading", "size", "theme", "variant", "linkProps", "linkAs"]);
111
159
 
112
- var imgRef = useRef(null); // Image loading state.
160
+ var _useState = useState(),
161
+ _useState2 = _slicedToArray(_useState, 2),
162
+ imgElement = _useState2[0],
163
+ setImgElement = _useState2[1]; // Image loading state.
164
+
113
165
 
114
- var loadingState = useImageLoad(image, imgRef);
166
+ var loadingState = useImageLoad(image, imgElement);
167
+ var isLoaded = loadingState === 'isLoaded';
115
168
  var isLoading = isLoadingProp || loadingState === 'isLoading';
116
- var hasError = loadingState === 'hasError';
169
+ var hasError = loadingState === 'hasError'; // Focus point.
170
+
171
+ var focusPointStyle = useFocusPointStyle(props, imgElement, isLoaded);
117
172
  var hasIconErrorFallback = hasError && typeof fallback === 'string';
118
173
  var hasCustomErrorFallback = hasError && !hasIconErrorFallback;
119
174
  var imageErrorStyle = {};
@@ -159,11 +214,8 @@ var Thumbnail = forwardRef(function (props, ref) {
159
214
  }), React.createElement("div", {
160
215
  className: "".concat(CLASSNAME, "__background")
161
216
  }, React.createElement("img", _extends({}, imgProps, {
162
- style: _objectSpread2({}, imgProps === null || imgProps === void 0 ? void 0 : imgProps.style, {}, imageErrorStyle, {
163
- // Focus point.
164
- objectPosition: getObjectPosition(aspectRatio, focusPoint)
165
- }),
166
- ref: mergeRefs(imgRef, propImgRef),
217
+ style: _objectSpread2({}, imgProps === null || imgProps === void 0 ? void 0 : imgProps.style, {}, imageErrorStyle, {}, focusPointStyle),
218
+ ref: mergeRefs(setImgElement, propImgRef),
167
219
  className: classnames("".concat(CLASSNAME, "__image"), isLoading && "".concat(CLASSNAME, "__image--is-loading")),
168
220
  crossOrigin: crossOrigin,
169
221
  src: image,
@@ -183,5 +235,5 @@ Thumbnail.displayName = COMPONENT_NAME;
183
235
  Thumbnail.className = CLASSNAME;
184
236
  Thumbnail.defaultProps = DEFAULT_PROPS;
185
237
 
186
- export { Thumbnail as T };
238
+ export { Thumbnail as T, useFocusPointStyle as u };
187
239
  //# sourceMappingURL=Thumbnail2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Thumbnail2.js","sources":["../../../src/components/thumbnail/useImageLoad.ts","../../../src/components/thumbnail/Thumbnail.tsx"],"sourcesContent":["import { RefObject, useEffect, useState } from 'react';\n\nexport type LoadingState = 'isLoading' | 'isLoaded' | 'hasError';\n\nfunction getState(img: HTMLImageElement | null | undefined, event?: Event) {\n // Error event occurred or image loaded empty.\n if (event?.type === 'error' || (img?.complete && (img?.naturalWidth === 0 || img?.naturalHeight === 0))) {\n return 'hasError';\n }\n // Image is undefined or incomplete.\n if (!img || !img.complete) {\n return 'isLoading';\n }\n // Else loaded.\n return 'isLoaded';\n}\n\nexport function useImageLoad(imageURL: string, imgRef?: RefObject<HTMLImageElement>): LoadingState {\n const [state, setState] = useState<LoadingState>(getState(imgRef?.current));\n\n // Update state when changing image URL or DOM reference.\n useEffect(() => {\n setState(getState(imgRef?.current));\n }, [imageURL, imgRef]);\n\n // Listen to `load` and `error` event on image\n useEffect(() => {\n const img = imgRef?.current;\n if (!img) return undefined;\n const update = (event?: Event) => setState(getState(img, event));\n img.addEventListener('load', update);\n img.addEventListener('error', update);\n return () => {\n img.removeEventListener('load', update);\n img.removeEventListener('error', update);\n };\n }, [imgRef, imgRef?.current?.src]);\n\n return state;\n}\n","import React, {\n CSSProperties,\n forwardRef,\n ImgHTMLAttributes,\n KeyboardEventHandler,\n MouseEventHandler,\n ReactElement,\n ReactNode,\n Ref,\n useRef,\n} from 'react';\nimport classNames from 'classnames';\n\nimport { AspectRatio, HorizontalAlignment, Icon, Size, Theme } from '@lumx/react';\n\nimport { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';\n\nimport { mdiImageBroken } from '@lumx/icons';\nimport { mergeRefs } from '@lumx/react/utils/mergeRefs';\nimport { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';\nimport { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';\n\ntype ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;\n\n/**\n * Defines the props of the component.\n */\nexport interface ThumbnailProps extends GenericProps {\n /** Alignment of the thumbnail in it's parent (requires flex parent). */\n align?: HorizontalAlignment;\n /** Image alternative text. */\n alt: string;\n /** Image aspect ratio. */\n aspectRatio?: AspectRatio;\n /** Badge. */\n badge?: ReactElement;\n /** Image cross origin resource policy. */\n crossOrigin?: ImgHTMLProps['crossOrigin'];\n /** Fallback icon (SVG path) or react node when image fails to load. */\n fallback?: string | ReactNode;\n /** Whether the thumbnail should fill it's parent size (requires flex parent) or not. */\n fillHeight?: boolean;\n /** Apply relative vertical and horizontal shift (from -1 to 1) on the image position inside the thumbnail. */\n focusPoint?: FocusPoint;\n /** Image URL. */\n image: string;\n /** Props to inject into the native <img> element. */\n imgProps?: ImgHTMLProps;\n /** Reference to the native <img> element. */\n imgRef?: Ref<HTMLImageElement>;\n /** Set to true to force the display of the loading skeleton. */\n isLoading?: boolean;\n /** Size variant of the component. */\n size?: ThumbnailSize;\n /** Image loading mode. */\n loading?: ImgHTMLProps['loading'];\n /** On click callback. */\n onClick?: MouseEventHandler<HTMLDivElement>;\n /** On key press callback. */\n onKeyPress?: KeyboardEventHandler<HTMLDivElement>;\n /** Theme adapting the component to light or dark background. */\n theme?: Theme;\n /** Variant of the component. */\n variant?: ThumbnailVariant;\n /** Props to pass to the link wrapping the thumbnail. */\n linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;\n /** Custom react component for the link (can be used to inject react router Link). */\n linkAs?: 'a' | any;\n}\n\n/**\n * Component display name.\n */\nconst COMPONENT_NAME = 'Thumbnail';\n\n/**\n * Component default class name and class prefix.\n */\nconst CLASSNAME = getRootClassName(COMPONENT_NAME);\n\n/**\n * Component default props.\n */\nconst DEFAULT_PROPS: Partial<ThumbnailProps> = {\n fallback: mdiImageBroken,\n loading: 'lazy',\n theme: Theme.light,\n};\n\nfunction getObjectPosition(aspectRatio: AspectRatio, focusPoint?: FocusPoint) {\n if (aspectRatio === AspectRatio.original || (!focusPoint?.y && !focusPoint?.x)) return undefined;\n const x = Math.round(Math.abs(((focusPoint?.x || 0) + 1) / 2) * 100);\n const y = Math.round(Math.abs(((focusPoint?.y || 0) - 1) / 2) * 100);\n return `${x}% ${y}%`;\n}\n\n/**\n * Thumbnail component.\n *\n * @param props Component props.\n * @param ref Component ref.\n * @return React element.\n */\nexport const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {\n const {\n align,\n alt,\n aspectRatio = AspectRatio.original,\n badge,\n className,\n crossOrigin,\n fallback,\n fillHeight,\n focusPoint,\n image,\n imgProps,\n imgRef: propImgRef,\n isLoading: isLoadingProp,\n loading,\n size,\n theme,\n variant,\n linkProps,\n linkAs,\n ...forwardedProps\n } = props;\n const imgRef = useRef<HTMLImageElement>(null);\n\n // Image loading state.\n const loadingState = useImageLoad(image, imgRef);\n const isLoading = isLoadingProp || loadingState === 'isLoading';\n const hasError = loadingState === 'hasError';\n\n const hasIconErrorFallback = hasError && typeof fallback === 'string';\n const hasCustomErrorFallback = hasError && !hasIconErrorFallback;\n const imageErrorStyle: CSSProperties = {};\n if (hasIconErrorFallback) {\n // Keep the image layout on icon fallback.\n imageErrorStyle.visibility = 'hidden';\n } else if (hasCustomErrorFallback) {\n // Remove the image on custom fallback.\n imageErrorStyle.display = 'none';\n }\n\n const isLink = Boolean(linkProps?.href || linkAs);\n const isButton = !!forwardedProps.onClick;\n const isClickable = isButton || isLink;\n\n let Wrapper: any = 'div';\n const wrapperProps = { ...forwardedProps };\n if (isLink) {\n Wrapper = linkAs || 'a';\n Object.assign(wrapperProps, linkProps);\n } else if (isButton) {\n Wrapper = 'button';\n }\n\n return (\n <Wrapper\n {...wrapperProps}\n ref={ref}\n className={classNames(\n linkProps?.className,\n className,\n handleBasicClasses({\n align,\n aspectRatio,\n prefix: CLASSNAME,\n size,\n theme,\n variant,\n isClickable,\n hasError,\n hasIconErrorFallback,\n hasCustomErrorFallback,\n isLoading,\n hasBadge: !!badge,\n }),\n fillHeight && `${CLASSNAME}--fill-height`,\n )}\n >\n <div className={`${CLASSNAME}__background`}>\n <img\n {...imgProps}\n style={{\n ...imgProps?.style,\n ...imageErrorStyle,\n // Focus point.\n objectPosition: getObjectPosition(aspectRatio, focusPoint),\n }}\n ref={mergeRefs(imgRef, propImgRef)}\n className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}\n crossOrigin={crossOrigin}\n src={image}\n alt={alt}\n loading={loading}\n />\n {!isLoading && hasError && (\n <div className={`${CLASSNAME}__fallback`}>\n {hasIconErrorFallback ? (\n <Icon icon={fallback as string} size={Size.xxs} theme={theme} />\n ) : (\n fallback\n )}\n </div>\n )}\n </div>\n {badge &&\n React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}\n </Wrapper>\n );\n});\nThumbnail.displayName = COMPONENT_NAME;\nThumbnail.className = CLASSNAME;\nThumbnail.defaultProps = DEFAULT_PROPS;\n"],"names":["getState","img","event","type","complete","naturalWidth","naturalHeight","useImageLoad","imageURL","imgRef","useState","current","state","setState","useEffect","undefined","update","addEventListener","removeEventListener","src","COMPONENT_NAME","CLASSNAME","getRootClassName","DEFAULT_PROPS","fallback","mdiImageBroken","loading","theme","Theme","light","getObjectPosition","aspectRatio","focusPoint","AspectRatio","original","y","x","Math","round","abs","Thumbnail","forwardRef","props","ref","align","alt","badge","className","crossOrigin","fillHeight","image","imgProps","propImgRef","isLoadingProp","isLoading","size","variant","linkProps","linkAs","forwardedProps","useRef","loadingState","hasError","hasIconErrorFallback","hasCustomErrorFallback","imageErrorStyle","visibility","display","isLink","Boolean","href","isButton","onClick","isClickable","Wrapper","wrapperProps","Object","assign","classNames","handleBasicClasses","prefix","hasBadge","style","objectPosition","mergeRefs","Size","xxs","React","cloneElement","displayName","defaultProps"],"mappings":"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"}
1
+ {"version":3,"file":"Thumbnail2.js","sources":["../../../src/components/thumbnail/useImageLoad.ts","../../../src/components/thumbnail/useFocusPointStyle.tsx","../../../src/components/thumbnail/Thumbnail.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nexport type LoadingState = 'isLoading' | 'isLoaded' | 'hasError';\n\nfunction getState(img: HTMLImageElement | null | undefined, event?: Event) {\n // Error event occurred or image loaded empty.\n if (event?.type === 'error' || (img?.complete && (img?.naturalWidth === 0 || img?.naturalHeight === 0))) {\n return 'hasError';\n }\n // Image is undefined or incomplete.\n if (!img || !img.complete) {\n return 'isLoading';\n }\n // Else loaded.\n return 'isLoaded';\n}\n\nexport function useImageLoad(imageURL: string, imgRef?: HTMLImageElement): LoadingState {\n const [state, setState] = useState<LoadingState>(getState(imgRef));\n\n // Update state when changing image URL or DOM reference.\n useEffect(() => {\n setState(getState(imgRef));\n }, [imageURL, imgRef]);\n\n // Listen to `load` and `error` event on image\n useEffect(() => {\n const img = imgRef;\n if (!img) return undefined;\n const update = (event?: Event) => setState(getState(img, event));\n img.addEventListener('load', update);\n img.addEventListener('error', update);\n return () => {\n img.removeEventListener('load', update);\n img.removeEventListener('error', update);\n };\n }, [imgRef, imgRef?.src]);\n\n return state;\n}\n","import { CSSProperties, useMemo } from 'react';\nimport { AspectRatio } from '@lumx/react/components';\nimport { ThumbnailProps } from '@lumx/react/components/thumbnail/Thumbnail';\n\nfunction shiftPosition(\n scale: number,\n containerSize: number,\n imageSize: number,\n focusSize: number | undefined,\n isVertical?: boolean,\n) {\n if (!focusSize) return 50;\n const focusFactor = (focusSize + 1) / 2;\n const scaledSize = Math.floor(imageSize / scale);\n let focus = Math.floor(focusFactor * scaledSize);\n if (isVertical) focus = scaledSize - focus;\n\n const containerCenter = Math.floor(containerSize / 2);\n let focusOffset = focus - containerCenter;\n const remainder = scaledSize - focus;\n if (remainder < containerCenter) focusOffset -= containerCenter - remainder;\n if (focusOffset < 0) return 0;\n\n return Math.min(100, Math.floor((focusOffset * 100) / containerSize));\n}\n\nexport const useFocusPointStyle = (\n { image, aspectRatio, focusPoint, imgProps: { width, height } = {} }: ThumbnailProps,\n element: HTMLImageElement | undefined,\n isLoaded: boolean,\n): CSSProperties => {\n // Get natural image size from imgProps or img element.\n const naturalSize = useMemo(() => {\n if (!image || aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return undefined;\n if (typeof width === 'number' && typeof height === 'number') return { width, height };\n if (element && isLoaded) return { width: element.naturalWidth, height: element.naturalHeight };\n return undefined;\n }, [aspectRatio, element, focusPoint?.x, focusPoint?.y, height, image, isLoaded, width]);\n\n // Compute focus point CSS style.\n return useMemo(() => {\n if (aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return {};\n if (element && naturalSize) {\n const actualWidth = element.offsetWidth;\n const actualHeight = element.offsetHeight;\n const heightScale = actualHeight / naturalSize.height;\n const widthScale = actualWidth / naturalSize.width;\n const x = shiftPosition(heightScale, actualWidth, naturalSize.width, focusPoint?.x);\n const y = shiftPosition(widthScale, actualHeight, naturalSize.height, focusPoint?.y, true);\n return { objectPosition: `${x}% ${y}%` };\n }\n // Focus point can't be computed yet => We hide the image until it can.\n return { visibility: 'hidden' };\n }, [aspectRatio, element, focusPoint, naturalSize]);\n};\n","import React, {\n CSSProperties,\n forwardRef,\n ImgHTMLAttributes,\n KeyboardEventHandler,\n MouseEventHandler,\n ReactElement,\n ReactNode,\n Ref,\n useState,\n} from 'react';\nimport classNames from 'classnames';\n\nimport { AspectRatio, HorizontalAlignment, Icon, Size, Theme } from '@lumx/react';\n\nimport { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';\n\nimport { mdiImageBroken } from '@lumx/icons';\nimport { mergeRefs } from '@lumx/react/utils/mergeRefs';\nimport { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';\nimport { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPointStyle';\nimport { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';\n\ntype ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;\n\n/**\n * Defines the props of the component.\n */\nexport interface ThumbnailProps extends GenericProps {\n /** Alignment of the thumbnail in it's parent (requires flex parent). */\n align?: HorizontalAlignment;\n /** Image alternative text. */\n alt: string;\n /** Image aspect ratio. */\n aspectRatio?: AspectRatio;\n /** Badge. */\n badge?: ReactElement;\n /** Image cross origin resource policy. */\n crossOrigin?: ImgHTMLProps['crossOrigin'];\n /** Fallback icon (SVG path) or react node when image fails to load. */\n fallback?: string | ReactNode;\n /** Whether the thumbnail should fill it's parent size (requires flex parent) or not. */\n fillHeight?: boolean;\n /** Apply relative vertical and horizontal shift (from -1 to 1) on the image position inside the thumbnail. */\n focusPoint?: FocusPoint;\n /** Image URL. */\n image: string;\n /** Props to inject into the native <img> element. */\n imgProps?: ImgHTMLProps;\n /** Reference to the native <img> element. */\n imgRef?: Ref<HTMLImageElement>;\n /** Set to true to force the display of the loading skeleton. */\n isLoading?: boolean;\n /** Size variant of the component. */\n size?: ThumbnailSize;\n /** Image loading mode. */\n loading?: ImgHTMLProps['loading'];\n /** On click callback. */\n onClick?: MouseEventHandler<HTMLDivElement>;\n /** On key press callback. */\n onKeyPress?: KeyboardEventHandler<HTMLDivElement>;\n /** Theme adapting the component to light or dark background. */\n theme?: Theme;\n /** Variant of the component. */\n variant?: ThumbnailVariant;\n /** Props to pass to the link wrapping the thumbnail. */\n linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;\n /** Custom react component for the link (can be used to inject react router Link). */\n linkAs?: 'a' | any;\n}\n\n/**\n * Component display name.\n */\nconst COMPONENT_NAME = 'Thumbnail';\n\n/**\n * Component default class name and class prefix.\n */\nconst CLASSNAME = getRootClassName(COMPONENT_NAME);\n\n/**\n * Component default props.\n */\nconst DEFAULT_PROPS: Partial<ThumbnailProps> = {\n fallback: mdiImageBroken,\n loading: 'lazy',\n theme: Theme.light,\n};\n\n/**\n * Thumbnail component.\n *\n * @param props Component props.\n * @param ref Component ref.\n * @return React element.\n */\nexport const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {\n const {\n align,\n alt,\n aspectRatio = AspectRatio.original,\n badge,\n className,\n crossOrigin,\n fallback,\n fillHeight,\n focusPoint,\n image,\n imgProps,\n imgRef: propImgRef,\n isLoading: isLoadingProp,\n loading,\n size,\n theme,\n variant,\n linkProps,\n linkAs,\n ...forwardedProps\n } = props;\n const [imgElement, setImgElement] = useState<HTMLImageElement>();\n\n // Image loading state.\n const loadingState = useImageLoad(image, imgElement);\n const isLoaded = loadingState === 'isLoaded';\n const isLoading = isLoadingProp || loadingState === 'isLoading';\n const hasError = loadingState === 'hasError';\n\n // Focus point.\n const focusPointStyle = useFocusPointStyle(props, imgElement, isLoaded);\n\n const hasIconErrorFallback = hasError && typeof fallback === 'string';\n const hasCustomErrorFallback = hasError && !hasIconErrorFallback;\n const imageErrorStyle: CSSProperties = {};\n if (hasIconErrorFallback) {\n // Keep the image layout on icon fallback.\n imageErrorStyle.visibility = 'hidden';\n } else if (hasCustomErrorFallback) {\n // Remove the image on custom fallback.\n imageErrorStyle.display = 'none';\n }\n\n const isLink = Boolean(linkProps?.href || linkAs);\n const isButton = !!forwardedProps.onClick;\n const isClickable = isButton || isLink;\n\n let Wrapper: any = 'div';\n const wrapperProps = { ...forwardedProps };\n if (isLink) {\n Wrapper = linkAs || 'a';\n Object.assign(wrapperProps, linkProps);\n } else if (isButton) {\n Wrapper = 'button';\n }\n\n return (\n <Wrapper\n {...wrapperProps}\n ref={ref}\n className={classNames(\n linkProps?.className,\n className,\n handleBasicClasses({\n align,\n aspectRatio,\n prefix: CLASSNAME,\n size,\n theme,\n variant,\n isClickable,\n hasError,\n hasIconErrorFallback,\n hasCustomErrorFallback,\n isLoading,\n hasBadge: !!badge,\n }),\n fillHeight && `${CLASSNAME}--fill-height`,\n )}\n >\n <div className={`${CLASSNAME}__background`}>\n <img\n {...imgProps}\n style={{\n ...imgProps?.style,\n ...imageErrorStyle,\n ...focusPointStyle,\n }}\n ref={mergeRefs(setImgElement, propImgRef)}\n className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}\n crossOrigin={crossOrigin}\n src={image}\n alt={alt}\n loading={loading}\n />\n {!isLoading && hasError && (\n <div className={`${CLASSNAME}__fallback`}>\n {hasIconErrorFallback ? (\n <Icon icon={fallback as string} size={Size.xxs} theme={theme} />\n ) : (\n fallback\n )}\n </div>\n )}\n </div>\n {badge &&\n React.cloneElement(badge, { className: classNames(`${CLASSNAME}__badge`, badge.props.className) })}\n </Wrapper>\n );\n});\nThumbnail.displayName = COMPONENT_NAME;\nThumbnail.className = CLASSNAME;\nThumbnail.defaultProps = DEFAULT_PROPS;\n"],"names":["getState","img","event","type","complete","naturalWidth","naturalHeight","useImageLoad","imageURL","imgRef","useState","state","setState","useEffect","undefined","update","addEventListener","removeEventListener","src","shiftPosition","scale","containerSize","imageSize","focusSize","isVertical","focusFactor","scaledSize","Math","floor","focus","containerCenter","focusOffset","remainder","min","useFocusPointStyle","element","isLoaded","image","aspectRatio","focusPoint","imgProps","width","height","naturalSize","useMemo","AspectRatio","original","x","y","actualWidth","offsetWidth","actualHeight","offsetHeight","heightScale","widthScale","objectPosition","visibility","COMPONENT_NAME","CLASSNAME","getRootClassName","DEFAULT_PROPS","fallback","mdiImageBroken","loading","theme","Theme","light","Thumbnail","forwardRef","props","ref","align","alt","badge","className","crossOrigin","fillHeight","propImgRef","isLoadingProp","isLoading","size","variant","linkProps","linkAs","forwardedProps","imgElement","setImgElement","loadingState","hasError","focusPointStyle","hasIconErrorFallback","hasCustomErrorFallback","imageErrorStyle","display","isLink","Boolean","href","isButton","onClick","isClickable","Wrapper","wrapperProps","Object","assign","classNames","handleBasicClasses","prefix","hasBadge","style","mergeRefs","Size","xxs","React","cloneElement","displayName","defaultProps"],"mappings":"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"}
@@ -9,6 +9,6 @@ import 'lodash/kebabCase';
9
9
  import 'lodash/noop';
10
10
  import './mergeRefs.js';
11
11
  import './Icon2.js';
12
- export { T as Thumbnail } from './Thumbnail2.js';
12
+ export { T as Thumbnail, u as useFocusPointStyle } from './Thumbnail2.js';
13
13
  export { T as ThumbnailAspectRatio, a as ThumbnailVariant } from './types.js';
14
14
  //# sourceMappingURL=thumbnail.js.map
@@ -1,5 +1,6 @@
1
1
  import { _ as _objectSpread2 } from './_rollupPluginBabelHelpers.js';
2
2
  import { AspectRatio } from './components.js';
3
+ import 'react';
3
4
 
4
5
  /**
5
6
  * All available aspect ratios.
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../../src/components/thumbnail/types.ts"],"sourcesContent":["import React from 'react';\nimport { AspectRatio, Size } from '@lumx/react';\nimport { ValueOf } from '@lumx/react/utils';\n\n/**\n * Focal point using vertical alignment, horizontal alignment or coordinates (from -1 to 1).\n */\nexport type FocusPoint = { x?: number; y?: number };\n\n/**\n * Loading attribute is not yet supported in typescript, so we need\n * to add it in order to avoid a ts error.\n * https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/ADVANCED.md#adding-non-standard-attributes\n */\ndeclare module 'react' {\n interface ImgHTMLAttributes<T> extends React.HTMLAttributes<T> {\n loading?: 'eager' | 'lazy';\n }\n}\n\n/**\n * All available aspect ratios.\n * @deprecated\n */\nexport const ThumbnailAspectRatio: Record<string, AspectRatio> = { ...AspectRatio };\n\n/**\n * Thumbnail sizes.\n */\nexport type ThumbnailSize = Extract<Size, 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'>;\n\n/**\n * Thumbnail variants.\n */\nexport const ThumbnailVariant = {\n squared: 'squared',\n rounded: 'rounded',\n} as const;\nexport type ThumbnailVariant = ValueOf<typeof ThumbnailVariant>;\n"],"names":["ThumbnailAspectRatio","AspectRatio","ThumbnailVariant","squared","rounded"],"mappings":";;;AAoBA;;;;IAIaA,oBAAiD,sBAAQC,WAAR;AAE9D;;;;AAKA;;;IAGaC,gBAAgB,GAAG;AAC5BC,EAAAA,OAAO,EAAE,SADmB;AAE5BC,EAAAA,OAAO,EAAE;AAFmB;;;;"}
1
+ {"version":3,"file":"types.js","sources":["../../../src/components/thumbnail/types.ts"],"sourcesContent":["import React from 'react';\nimport { AspectRatio, Size } from '@lumx/react';\nimport { ValueOf } from '@lumx/react/utils';\n\n/**\n * Focal point using vertical alignment, horizontal alignment or coordinates (from -1 to 1).\n */\nexport type FocusPoint = { x?: number; y?: number };\n\n/**\n * Loading attribute is not yet supported in typescript, so we need\n * to add it in order to avoid a ts error.\n * https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/ADVANCED.md#adding-non-standard-attributes\n */\ndeclare module 'react' {\n interface ImgHTMLAttributes<T> extends React.HTMLAttributes<T> {\n loading?: 'eager' | 'lazy';\n }\n}\n\n/**\n * All available aspect ratios.\n * @deprecated\n */\nexport const ThumbnailAspectRatio: Record<string, AspectRatio> = { ...AspectRatio };\n\n/**\n * Thumbnail sizes.\n */\nexport type ThumbnailSize = Extract<Size, 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'>;\n\n/**\n * Thumbnail variants.\n */\nexport const ThumbnailVariant = {\n squared: 'squared',\n rounded: 'rounded',\n} as const;\nexport type ThumbnailVariant = ValueOf<typeof ThumbnailVariant>;\n"],"names":["ThumbnailAspectRatio","AspectRatio","ThumbnailVariant","squared","rounded"],"mappings":";;;;AAoBA;;;;IAIaA,oBAAiD,sBAAQC,WAAR;AAE9D;;;;AAKA;;;IAGaC,gBAAgB,GAAG;AAC5BC,EAAAA,OAAO,EAAE,SADmB;AAE5BC,EAAAA,OAAO,EAAE;AAFmB;;;;"}
package/esm/index.js CHANGED
@@ -80,7 +80,7 @@ export { S as Switch } from './_internal/Switch2.js';
80
80
  export { T as Table, a as TableBody, d as TableCell, c as TableCellVariant, e as TableHeader, f as TableRow, b as ThOrder } from './_internal/TableRow.js';
81
81
  export { c as Tab, b as TabList, a as TabListLayout, d as TabPanel, T as TabProvider } from './_internal/TabPanel.js';
82
82
  export { T as TextField } from './_internal/TextField.js';
83
- export { T as Thumbnail } from './_internal/Thumbnail2.js';
83
+ export { T as Thumbnail, u as useFocusPointStyle } from './_internal/Thumbnail2.js';
84
84
  export { T as ThumbnailAspectRatio, a as ThumbnailVariant } from './_internal/types.js';
85
85
  export { T as Toolbar } from './_internal/Toolbar2.js';
86
86
  export { T as Tooltip } from './_internal/Tooltip2.js';
package/package.json CHANGED
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^2.1.9-alpha-thumbnail12",
11
- "@lumx/icons": "^2.1.9-alpha-thumbnail12",
10
+ "@lumx/core": "^2.1.9-alpha-thumbnail13",
11
+ "@lumx/icons": "^2.1.9-alpha-thumbnail13",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.2.6",
@@ -120,6 +120,6 @@
120
120
  "build:storybook": "cd storybook && ./build"
121
121
  },
122
122
  "sideEffects": false,
123
- "version": "2.1.9-alpha-thumbnail12",
124
- "gitHead": "ba0496d400184d28c49deb8ac70e3c1181225a45"
123
+ "version": "2.1.9-alpha-thumbnail13",
124
+ "gitHead": "18619b44fecc3b26cbbed5ff531b538b4d7ca550"
125
125
  }
@@ -22,6 +22,10 @@ import classNames from 'classnames';
22
22
 
23
23
  export default { title: 'LumX components/thumbnail/Thumbnail' };
24
24
 
25
+ const Resizable = ({ initialSize: { width, height }, children }: any) => (
26
+ <div style={{ border: '1px solid red', overflow: 'hidden', resize: 'both', width, height }}>{children}</div>
27
+ );
28
+
25
29
  /** Default thumbnail props (editable via knobs) */
26
30
  export const Default = ({ theme }: any) => {
27
31
  const alt = text('Alternative text', 'Image alt text');
@@ -79,6 +83,41 @@ export const WithBadge = () => {
79
83
  );
80
84
  };
81
85
 
86
+ export const FocusPoint = () => {
87
+ const focusPoint = { x: focusKnob('Focus X ', -0.2), y: focusKnob('Focus Y', -0.3) };
88
+ const aspectRatio = enumKnob('Aspect ratio', [undefined, ...Object.values(AspectRatio)], AspectRatio.free);
89
+ const fillHeight = aspectRatio === AspectRatio.free;
90
+ return (
91
+ <>
92
+ <small>Focus point will delay the display of the image if the original image size is not accessible.</small>
93
+
94
+ <Resizable initialSize={{ height: 200, width: 300 }}>
95
+ <Thumbnail
96
+ alt="Image"
97
+ image={IMAGES.portrait1}
98
+ aspectRatio={aspectRatio}
99
+ fillHeight={fillHeight}
100
+ focusPoint={focusPoint}
101
+ style={{ width: '100%' }}
102
+ />
103
+ </Resizable>
104
+
105
+ <small>Providing the width & height in imgProps should avoid the delay shown above</small>
106
+ <Resizable initialSize={{ height: 200, width: 300 }}>
107
+ <Thumbnail
108
+ alt="Image"
109
+ image={IMAGES.portrait2}
110
+ imgProps={IMAGE_SIZES.portrait2}
111
+ fillHeight={fillHeight}
112
+ aspectRatio={aspectRatio}
113
+ focusPoint={focusPoint}
114
+ style={{ width: '100%' }}
115
+ />
116
+ </Resizable>
117
+ </>
118
+ );
119
+ };
120
+
82
121
  export const Clickable = () => (
83
122
  <Thumbnail alt="Click me" image={imageKnob()} size={sizeKnob('Size', Size.xxl)} onClick={action('onClick')} />
84
123
  );
@@ -7,7 +7,7 @@ import React, {
7
7
  ReactElement,
8
8
  ReactNode,
9
9
  Ref,
10
- useRef,
10
+ useState,
11
11
  } from 'react';
12
12
  import classNames from 'classnames';
13
13
 
@@ -18,6 +18,7 @@ import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/
18
18
  import { mdiImageBroken } from '@lumx/icons';
19
19
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
20
20
  import { useImageLoad } from '@lumx/react/components/thumbnail/useImageLoad';
21
+ import { useFocusPointStyle } from '@lumx/react/components/thumbnail/useFocusPointStyle';
21
22
  import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types';
22
23
 
23
24
  type ImgHTMLProps = ImgHTMLAttributes<HTMLImageElement>;
@@ -87,13 +88,6 @@ const DEFAULT_PROPS: Partial<ThumbnailProps> = {
87
88
  theme: Theme.light,
88
89
  };
89
90
 
90
- function getObjectPosition(aspectRatio: AspectRatio, focusPoint?: FocusPoint) {
91
- if (aspectRatio === AspectRatio.original || (!focusPoint?.y && !focusPoint?.x)) return undefined;
92
- const x = Math.round(Math.abs(((focusPoint?.x || 0) + 1) / 2) * 100);
93
- const y = Math.round(Math.abs(((focusPoint?.y || 0) - 1) / 2) * 100);
94
- return `${x}% ${y}%`;
95
- }
96
-
97
91
  /**
98
92
  * Thumbnail component.
99
93
  *
@@ -124,13 +118,17 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
124
118
  linkAs,
125
119
  ...forwardedProps
126
120
  } = props;
127
- const imgRef = useRef<HTMLImageElement>(null);
121
+ const [imgElement, setImgElement] = useState<HTMLImageElement>();
128
122
 
129
123
  // Image loading state.
130
- const loadingState = useImageLoad(image, imgRef);
124
+ const loadingState = useImageLoad(image, imgElement);
125
+ const isLoaded = loadingState === 'isLoaded';
131
126
  const isLoading = isLoadingProp || loadingState === 'isLoading';
132
127
  const hasError = loadingState === 'hasError';
133
128
 
129
+ // Focus point.
130
+ const focusPointStyle = useFocusPointStyle(props, imgElement, isLoaded);
131
+
134
132
  const hasIconErrorFallback = hasError && typeof fallback === 'string';
135
133
  const hasCustomErrorFallback = hasError && !hasIconErrorFallback;
136
134
  const imageErrorStyle: CSSProperties = {};
@@ -185,10 +183,9 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
185
183
  style={{
186
184
  ...imgProps?.style,
187
185
  ...imageErrorStyle,
188
- // Focus point.
189
- objectPosition: getObjectPosition(aspectRatio, focusPoint),
186
+ ...focusPointStyle,
190
187
  }}
191
- ref={mergeRefs(imgRef, propImgRef)}
188
+ ref={mergeRefs(setImgElement, propImgRef)}
192
189
  className={classNames(`${CLASSNAME}__image`, isLoading && `${CLASSNAME}__image--is-loading`)}
193
190
  crossOrigin={crossOrigin}
194
191
  src={image}
@@ -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,55 @@
1
+ import { CSSProperties, useMemo } from 'react';
2
+ import { AspectRatio } from '@lumx/react/components';
3
+ import { ThumbnailProps } from '@lumx/react/components/thumbnail/Thumbnail';
4
+
5
+ function shiftPosition(
6
+ scale: number,
7
+ containerSize: number,
8
+ imageSize: number,
9
+ focusSize: number | undefined,
10
+ isVertical?: boolean,
11
+ ) {
12
+ if (!focusSize) return 50;
13
+ const focusFactor = (focusSize + 1) / 2;
14
+ const scaledSize = Math.floor(imageSize / scale);
15
+ let focus = Math.floor(focusFactor * scaledSize);
16
+ if (isVertical) focus = scaledSize - focus;
17
+
18
+ const containerCenter = Math.floor(containerSize / 2);
19
+ let focusOffset = focus - containerCenter;
20
+ const remainder = scaledSize - focus;
21
+ if (remainder < containerCenter) focusOffset -= containerCenter - remainder;
22
+ if (focusOffset < 0) return 0;
23
+
24
+ return Math.min(100, Math.floor((focusOffset * 100) / containerSize));
25
+ }
26
+
27
+ export const useFocusPointStyle = (
28
+ { image, aspectRatio, focusPoint, imgProps: { width, height } = {} }: ThumbnailProps,
29
+ element: HTMLImageElement | undefined,
30
+ isLoaded: boolean,
31
+ ): CSSProperties => {
32
+ // Get natural image size from imgProps or img element.
33
+ const naturalSize = useMemo(() => {
34
+ if (!image || aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return undefined;
35
+ if (typeof width === 'number' && typeof height === 'number') return { width, height };
36
+ if (element && isLoaded) return { width: element.naturalWidth, height: element.naturalHeight };
37
+ return undefined;
38
+ }, [aspectRatio, element, focusPoint?.x, focusPoint?.y, height, image, isLoaded, width]);
39
+
40
+ // Compute focus point CSS style.
41
+ return useMemo(() => {
42
+ if (aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return {};
43
+ if (element && naturalSize) {
44
+ const actualWidth = element.offsetWidth;
45
+ const actualHeight = element.offsetHeight;
46
+ const heightScale = actualHeight / naturalSize.height;
47
+ const widthScale = actualWidth / naturalSize.width;
48
+ const x = shiftPosition(heightScale, actualWidth, naturalSize.width, focusPoint?.x);
49
+ const y = shiftPosition(widthScale, actualHeight, naturalSize.height, focusPoint?.y, true);
50
+ return { objectPosition: `${x}% ${y}%` };
51
+ }
52
+ // Focus point can't be computed yet => We hide the image until it can.
53
+ return { visibility: 'hidden' };
54
+ }, [aspectRatio, element, focusPoint, naturalSize]);
55
+ };
@@ -1,4 +1,4 @@
1
- import { RefObject, useEffect, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
3
  export type LoadingState = 'isLoading' | 'isLoaded' | 'hasError';
4
4
 
@@ -15,17 +15,17 @@ function getState(img: HTMLImageElement | null | undefined, event?: Event) {
15
15
  return 'isLoaded';
16
16
  }
17
17
 
18
- export function useImageLoad(imageURL: string, imgRef?: RefObject<HTMLImageElement>): LoadingState {
19
- const [state, setState] = useState<LoadingState>(getState(imgRef?.current));
18
+ export function useImageLoad(imageURL: string, imgRef?: HTMLImageElement): LoadingState {
19
+ const [state, setState] = useState<LoadingState>(getState(imgRef));
20
20
 
21
21
  // Update state when changing image URL or DOM reference.
22
22
  useEffect(() => {
23
- setState(getState(imgRef?.current));
23
+ setState(getState(imgRef));
24
24
  }, [imageURL, imgRef]);
25
25
 
26
26
  // Listen to `load` and `error` event on image
27
27
  useEffect(() => {
28
- const img = imgRef?.current;
28
+ const img = imgRef;
29
29
  if (!img) return undefined;
30
30
  const update = (event?: Event) => setState(getState(img, event));
31
31
  img.addEventListener('load', update);
@@ -34,7 +34,7 @@ export function useImageLoad(imageURL: string, imgRef?: RefObject<HTMLImageEleme
34
34
  img.removeEventListener('load', update);
35
35
  img.removeEventListener('error', update);
36
36
  };
37
- }, [imgRef, imgRef?.current?.src]);
37
+ }, [imgRef, imgRef?.src]);
38
38
 
39
39
  return state;
40
40
  }
@@ -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 });
package/types.d.ts CHANGED
@@ -2433,6 +2433,7 @@ export interface TextFieldProps extends GenericProps {
2433
2433
  * @return React element.
2434
2434
  */
2435
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;
2436
2437
  /**
2437
2438
  * Defines the props of the component.
2438
2439
  */