@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
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export default { title: 'LumX components/badge/Badge Demos' };
5
5
 
6
+ export { App as Colors } from './colors';
6
7
  export { App as Icon } from './icon';
7
8
  export { App as Label } from './label';
8
9
  export { App as Thumbnail } from './thumbnail';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * File generated when storybook is started. Do not edit directly!
3
+ */
4
+ export default { title: 'LumX components/flag/Flag Demos' };
5
+
6
+ export { App as Flag } from './flag';
@@ -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';
@@ -4,6 +4,7 @@
4
4
  export default { title: 'LumX components/thumbnail/Thumbnail Demos' };
5
5
 
6
6
  export { App as Combined } from './combined';
7
+ export { App as LoadingError } from './loading-error';
7
8
  export { App as Ratios } from './ratios';
8
9
  export { App as Sizes } from './sizes';
9
10
  export { App as Variants } from './variants';
@@ -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
+ };
@@ -0,0 +1,7 @@
1
+ /* eslint-disable jsx-a11y/anchor-has-content,jsx-a11y/anchor-is-valid */
2
+ import React from 'react';
3
+
4
+ /**
5
+ * Example custom link that can be used in linkAs props.
6
+ */
7
+ export const CustomLink: any = ({ ...props }) => <a href="#" data-custom-link {...props} />;
package/types.d.ts CHANGED
@@ -430,6 +430,10 @@ export interface AvatarProps extends GenericProps {
430
430
  badge?: ReactElement;
431
431
  /** Image URL. */
432
432
  image: string;
433
+ /** Props to pass to the link wrapping the thumbnail. */
434
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
435
+ /** Custom react component for the link (can be used to inject react router Link). */
436
+ linkAs?: "a" | any;
433
437
  /** On click callback. */
434
438
  onClick?: MouseEventHandler<HTMLDivElement>;
435
439
  /** On key press callback. */
@@ -1233,6 +1237,8 @@ export interface ThumbnailProps extends GenericProps {
1233
1237
  imgProps?: ImgHTMLProps;
1234
1238
  /** Reference to the native <img> element. */
1235
1239
  imgRef?: Ref<HTMLImageElement>;
1240
+ /** Set to true to force the display of the loading skeleton. */
1241
+ isLoading?: boolean;
1236
1242
  /** Size variant of the component. */
1237
1243
  size?: ThumbnailSize;
1238
1244
  /** Image loading mode. */
@@ -1245,6 +1251,10 @@ export interface ThumbnailProps extends GenericProps {
1245
1251
  theme?: Theme;
1246
1252
  /** Variant of the component. */
1247
1253
  variant?: ThumbnailVariant;
1254
+ /** Props to pass to the link wrapping the thumbnail. */
1255
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
1256
+ /** Custom react component for the link (can be used to inject react router Link). */
1257
+ linkAs?: "a" | any;
1248
1258
  }
1249
1259
  /**
1250
1260
  * Thumbnail component.
@@ -2427,6 +2437,7 @@ export interface TextFieldProps extends GenericProps {
2427
2437
  * @return React element.
2428
2438
  */
2429
2439
  export declare const TextField: Comp<TextFieldProps, HTMLDivElement>;
2440
+ export declare const useFocusPointStyle: ({ image, aspectRatio, focusPoint, imgProps: { width, height } }: ThumbnailProps, element: HTMLImageElement | undefined, isLoaded: boolean) => CSSProperties;
2430
2441
  /**
2431
2442
  * Defines the props of the component.
2432
2443
  */
@@ -2521,16 +2532,22 @@ export declare type UserBlockSize = Extract<Size, "s" | "m" | "l">;
2521
2532
  export interface UserBlockProps extends GenericProps {
2522
2533
  /** Props to pass to the avatar. */
2523
2534
  avatarProps?: AvatarProps;
2524
- /** Simple action toolbar content. */
2525
- simpleAction?: ReactNode;
2526
- /** Multiple action toolbar content. */
2527
- multipleActions?: ReactNode;
2528
2535
  /** Additional fields used to describe the user. */
2529
2536
  fields?: string[];
2537
+ /** Props to pass to the link wrapping the avatar thumbnail. */
2538
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
2539
+ /** Custom react component for the link (can be used to inject react router Link). */
2540
+ linkAs?: "a" | any;
2541
+ /** Multiple action toolbar content. */
2542
+ multipleActions?: ReactNode;
2530
2543
  /** User name. */
2531
2544
  name?: string;
2545
+ /** Props to pass to the name block. */
2546
+ nameProps?: GenericProps;
2532
2547
  /** Orientation. */
2533
2548
  orientation?: Orientation;
2549
+ /** Simple action toolbar content. */
2550
+ simpleAction?: ReactNode;
2534
2551
  /** Size variant. */
2535
2552
  size?: UserBlockSize;
2536
2553
  /** Theme adapting the component to light or dark background. */
@@ -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;;;;"}
@@ -1,26 +0,0 @@
1
- import { KeyboardEventHandler, MouseEventHandler, useMemo } from 'react';
2
-
3
- interface Props {
4
- role: 'button';
5
- tabIndex: 0;
6
- onClick: MouseEventHandler;
7
- onKeyPress: KeyboardEventHandler;
8
- }
9
-
10
- export const useClickable = ({ onClick, onKeyPress }: { onClick?: any; onKeyPress?: any }): Props | undefined => {
11
- return useMemo(() => {
12
- if (!onClick) return undefined;
13
-
14
- return {
15
- role: 'button',
16
- tabIndex: 0,
17
- onClick,
18
- onKeyPress(event) {
19
- onKeyPress?.(event);
20
- if (event.key === 'Enter' || event.key === ' ') {
21
- onClick?.();
22
- }
23
- },
24
- };
25
- }, [onClick, onKeyPress]);
26
- };
@@ -1,154 +0,0 @@
1
- import { CSSProperties, MutableRefObject, RefObject, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
- import { useOnResize } from '@lumx/react/hooks/useOnResize';
3
- import { AspectRatio } from '@lumx/react';
4
- import { clamp } from '@lumx/react/utils/clamp';
5
- import { LoadingState } from '@lumx/react/components/thumbnail/useImageLoad';
6
- import { Callback } from '@lumx/react/utils';
7
- import { isEqual } from 'lodash';
8
- import { FocusPoint } from './types';
9
-
10
- const IMG_STYLES: CSSProperties = {
11
- bottom: 0,
12
- left: 0,
13
- position: 'absolute',
14
- right: 0,
15
- top: 0,
16
- minHeight: '100%',
17
- minWidth: '100%',
18
- };
19
-
20
- const WRAPPER_STYLES: CSSProperties = {
21
- overflow: 'hidden',
22
- position: 'relative',
23
- };
24
-
25
- type Sizes = {
26
- imgWidth?: number;
27
- imgHeight?: number;
28
- containerWidth?: number;
29
- containerHeight?: number;
30
- aspectRatio?: AspectRatio;
31
- };
32
-
33
- function calculateSizes(
34
- imageElement?: HTMLImageElement | null | undefined,
35
- parentElement?: HTMLElement | null,
36
- aspectRatio?: AspectRatio,
37
- ): Sizes | undefined {
38
- if (!imageElement || !parentElement || !aspectRatio || aspectRatio === AspectRatio.original) return undefined;
39
- const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imageElement || { naturalWidth: 0, naturalHeight: 0 };
40
- const { width: containerWidth, height: containerHeight } = parentElement?.getBoundingClientRect() || {
41
- width: 0,
42
- height: 0,
43
- };
44
- return { imgWidth, imgHeight, containerWidth, containerHeight, aspectRatio };
45
- }
46
-
47
- const parseFocusPoint = (point: FocusPoint = {}): Required<FocusPoint> => {
48
- return { x: point.x ? clamp(point.x, -1, 1) : 0, y: point.y ? clamp(point.y, -1, 1) : 0 };
49
- };
50
-
51
- function calculateImageShift(
52
- containerToImageSizeRatio: number,
53
- containerSize: number,
54
- imageSize: number,
55
- focusSize: number,
56
- toMinus?: boolean,
57
- ) {
58
- const containerCenter = Math.floor(containerSize / 2); // Container center in px
59
- const focusFactor = (focusSize + 1) / 2; // Focus point of resize image in px
60
- const scaledImage = Math.floor(imageSize / containerToImageSizeRatio); // Can't use width() as images may be display:none
61
- let focus = Math.floor(focusFactor * scaledImage);
62
- if (toMinus) {
63
- focus = scaledImage - focus;
64
- }
65
- let focusOffset = focus - containerCenter; // Calculate difference between focus point and center
66
- const remainder = scaledImage - focus; // Reduce offset if necessary so image remains filled
67
- const containerRemainder = containerSize - containerCenter;
68
- if (remainder < containerRemainder) {
69
- focusOffset -= containerRemainder - remainder;
70
- }
71
- if (focusOffset < 0) {
72
- focusOffset = 0;
73
- }
74
- return (focusOffset * -100) / containerSize;
75
- }
76
-
77
- type Styles = { wrapper: CSSProperties; image: CSSProperties };
78
-
79
- function calculateImageStyle(sizes: Sizes, point: Required<FocusPoint>): Styles | undefined {
80
- if (!sizes.imgWidth || !sizes.imgHeight || !sizes.containerWidth || !sizes.containerHeight) {
81
- return undefined;
82
- }
83
-
84
- // Which is over by more?
85
- const widthRatio = sizes.imgWidth / sizes.containerWidth;
86
- const heightRatio = sizes.imgHeight / sizes.containerHeight;
87
-
88
- const image: CSSProperties = { ...IMG_STYLES };
89
-
90
- // Minimize image while still filling space
91
- if (sizes.imgWidth > sizes.containerWidth && sizes.imgHeight > sizes.containerHeight) {
92
- image[widthRatio > heightRatio ? 'maxHeight' : 'maxWidth'] = '100%';
93
- }
94
-
95
- if (widthRatio > heightRatio) {
96
- image.left = `${calculateImageShift(heightRatio, sizes.containerWidth, sizes.imgWidth, point.x)}%`;
97
- } else if (widthRatio < heightRatio) {
98
- image.top = `${calculateImageShift(widthRatio, sizes.containerHeight, sizes.imgHeight, point.y, true)}%`;
99
- }
100
-
101
- return {
102
- image,
103
- wrapper: WRAPPER_STYLES,
104
- };
105
- }
106
-
107
- /**
108
- * Hook that calculate CSS style to shift the image in it's container according to the focus point.
109
- */
110
- export const useFocusPoint = (options: {
111
- image: string;
112
- focusPoint?: FocusPoint;
113
- aspectRatio?: AspectRatio;
114
- imgRef: RefObject<HTMLImageElement>;
115
- loadingState: LoadingState;
116
- wrapper?: HTMLElement;
117
- }): Styles | undefined => {
118
- const { image, aspectRatio, focusPoint, imgRef, loadingState, wrapper } = options;
119
-
120
- const point = parseFocusPoint(focusPoint);
121
-
122
- const previousPoint = useRef<Required<FocusPoint>>();
123
- const previousSizes = useRef<Sizes>({});
124
- const [style, setStyle] = useState<Styles>();
125
-
126
- // Update style.
127
- const updateRef = useRef<Callback>(null);
128
- const update = useMemo(
129
- () => {
130
- const updateFunction = () => {
131
- const sizes = calculateSizes(imgRef?.current, wrapper, aspectRatio);
132
- if (!sizes || (isEqual(sizes, previousSizes.current) && isEqual(point, previousPoint.current))) {
133
- // Nothing changed.
134
- return;
135
- }
136
- setStyle(calculateImageStyle(sizes, point));
137
- previousPoint.current = point;
138
- previousSizes.current = sizes;
139
- };
140
- (updateRef as MutableRefObject<Callback>).current = updateFunction;
141
- return updateFunction;
142
- },
143
- // eslint-disable-next-line react-hooks/exhaustive-deps
144
- [...Object.values(point), imgRef, wrapper, aspectRatio],
145
- );
146
-
147
- // Update on image loaded.
148
- useLayoutEffect(update, [image, update, loadingState]);
149
-
150
- // Update on parent resize.
151
- useOnResize(wrapper, updateRef);
152
-
153
- return style;
154
- };