@lumx/react 3.8.1-alpha.0 → 3.8.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 (31) hide show
  1. package/index.d.ts +6 -63
  2. package/index.js +559 -1292
  3. package/index.js.map +1 -1
  4. package/package.json +3 -3
  5. package/src/components/image-block/ImageBlock.test.tsx +28 -0
  6. package/src/components/image-block/ImageBlock.tsx +5 -1
  7. package/src/components/image-block/ImageCaption.tsx +54 -8
  8. package/src/components/thumbnail/useFocusPointStyle.tsx +4 -3
  9. package/src/index.ts +0 -1
  10. package/src/utils/type.ts +0 -15
  11. package/src/components/image-lightbox/ImageLightbox.stories.tsx +0 -165
  12. package/src/components/image-lightbox/ImageLightbox.test.tsx +0 -253
  13. package/src/components/image-lightbox/ImageLightbox.tsx +0 -72
  14. package/src/components/image-lightbox/constants.ts +0 -11
  15. package/src/components/image-lightbox/index.ts +0 -2
  16. package/src/components/image-lightbox/internal/ImageSlide.tsx +0 -107
  17. package/src/components/image-lightbox/internal/ImageSlideshow.tsx +0 -173
  18. package/src/components/image-lightbox/internal/useAnimateScroll.ts +0 -55
  19. package/src/components/image-lightbox/internal/usePointerZoom.ts +0 -148
  20. package/src/components/image-lightbox/types.ts +0 -50
  21. package/src/components/image-lightbox/useImageLightbox.tsx +0 -130
  22. package/src/hooks/useElementSizeDependentOfWindowSize.ts +0 -32
  23. package/src/hooks/useImageSize.ts +0 -17
  24. package/src/stories/generated/ImageLightbox/Demos.stories.tsx +0 -6
  25. package/src/utils/DOM/findImage.tsx +0 -3
  26. package/src/utils/DOM/startViewTransition.ts +0 -56
  27. package/src/utils/browser/getPrefersReducedMotion.ts +0 -6
  28. package/src/utils/object/isEqual.test.ts +0 -25
  29. package/src/utils/object/isEqual.ts +0 -11
  30. package/src/utils/react/unref.ts +0 -7
  31. package/src/utils/unref.ts +0 -0
@@ -1,148 +0,0 @@
1
- import React from 'react';
2
- import type { Point } from '@lumx/react/utils/type';
3
- import type { useAnimateScroll } from './useAnimateScroll';
4
-
5
- /**
6
- * Listen to mouse wheel + ctrl and multi-pointer pinch to zoom
7
- */
8
- export function usePointerZoom(
9
- scrollAreaRef: React.RefObject<HTMLDivElement>,
10
- onScaleChange: ((value: number) => void) | undefined,
11
- animateScroll: ReturnType<typeof useAnimateScroll>,
12
- ) {
13
- const [isPointerZooming, setPointerZooming] = React.useState(false);
14
- React.useEffect(() => {
15
- const scrollArea = scrollAreaRef.current as HTMLDivElement;
16
- if (!scrollArea || !onScaleChange) {
17
- return undefined;
18
- }
19
-
20
- let animationFrame: number | null;
21
- let zoomStateTimeoutId: ReturnType<typeof setTimeout> | null;
22
-
23
- function updateScaleOnNextFrame(newScale: number, mousePosition: Point): void {
24
- // Cancel previously scheduled frame
25
- if (animationFrame) cancelAnimationFrame(animationFrame);
26
-
27
- // Cancel previously scheduled zoom state change
28
- if (zoomStateTimeoutId) clearTimeout(zoomStateTimeoutId);
29
-
30
- function nextFrame() {
31
- setPointerZooming(true);
32
- onScaleChange?.(newScale);
33
-
34
- animationFrame = null;
35
- // Wait a bit before indicating the pointer zooming is finished
36
- zoomStateTimeoutId = setTimeout(() => setPointerZooming(false), 100);
37
- }
38
- animationFrame = requestAnimationFrame(nextFrame);
39
-
40
- // Animate scroll in parallel (centering on the current mouse position)
41
- animateScroll(mousePosition, {
42
- width: scrollArea.scrollWidth,
43
- height: scrollArea.scrollHeight,
44
- });
45
- }
46
-
47
- function onWheel(event: WheelEvent) {
48
- if (!event.ctrlKey) {
49
- return;
50
- }
51
- event.preventDefault();
52
- const newScale = Math.exp(-event.deltaY / 50);
53
-
54
- // Update scale on next frame (focused on the mouse position)
55
- updateScaleOnNextFrame(newScale, {
56
- x: event.pageX,
57
- y: event.pageY,
58
- });
59
- }
60
-
61
- const activePointers: Record<PointerEvent['pointerId'], PointerEvent> = {};
62
- let prevDistance: number | null = null;
63
- let previousCenterPoint: Point | null = null;
64
-
65
- function onPointerDown(event: PointerEvent) {
66
- activePointers[event.pointerId] = event;
67
- }
68
- function onPointerMove(event: PointerEvent) {
69
- // Update pointer in cache
70
- if (activePointers[event.pointerId]) {
71
- activePointers[event.pointerId] = event;
72
- }
73
-
74
- const pointers = Object.values(activePointers);
75
-
76
- // Make sure we run computation on one of the pointer in the group
77
- if (pointers[0].pointerId !== event.pointerId) {
78
- return;
79
- }
80
-
81
- // Centered point between all pointers
82
- const centerPoint: Point = {
83
- x: pointers.reduce((x, { clientX }) => x + clientX, 0) / pointers.length,
84
- y: pointers.reduce((y, { clientY }) => y + clientY, 0) / pointers.length,
85
- };
86
-
87
- // Movement of the center point
88
- const deltaCenterPoint = previousCenterPoint && {
89
- left: previousCenterPoint.x - centerPoint.x,
90
- top: previousCenterPoint.y - centerPoint.y,
91
- };
92
-
93
- // Pan X & Y
94
- if (deltaCenterPoint) {
95
- // Apply movement of the center point to the scroll
96
- scrollArea.scrollBy({
97
- top: deltaCenterPoint.top / 2,
98
- left: deltaCenterPoint.left / 2,
99
- });
100
- }
101
-
102
- // Pinch to zoom
103
- if (pointers.length === 2) {
104
- const [pointer1, pointer2] = pointers;
105
- const distance = Math.hypot(pointer2.clientX - pointer1.clientX, pointer2.clientY - pointer1.clientY);
106
-
107
- if (prevDistance && deltaCenterPoint) {
108
- const delta = prevDistance - distance;
109
- const absDelta = Math.abs(delta);
110
-
111
- // Zoom only if we are "pinching" more than we are moving the pointers
112
- if (absDelta > Math.abs(deltaCenterPoint.left) && absDelta > Math.abs(deltaCenterPoint.top)) {
113
- // Update scale on next frame (focused on the center point between the two pointers)
114
- const newScale = Math.exp(-delta / 100);
115
- updateScaleOnNextFrame(newScale, centerPoint);
116
- }
117
- }
118
-
119
- prevDistance = distance;
120
- }
121
-
122
- previousCenterPoint = centerPoint;
123
- }
124
- function onPointerUp(event: PointerEvent) {
125
- prevDistance = null;
126
- previousCenterPoint = null;
127
- delete activePointers[event.pointerId];
128
- }
129
-
130
- scrollArea.addEventListener('wheel', onWheel, { passive: false });
131
- const isMultiTouch = navigator.maxTouchPoints >= 2;
132
- if (isMultiTouch) {
133
- scrollArea.addEventListener('pointerdown', onPointerDown);
134
- scrollArea.addEventListener('pointermove', onPointerMove);
135
- scrollArea.addEventListener('pointerup', onPointerUp);
136
- }
137
- return () => {
138
- scrollArea.removeEventListener('wheel', onWheel);
139
- if (isMultiTouch) {
140
- scrollArea.removeEventListener('pointerdown', onPointerDown);
141
- scrollArea.removeEventListener('pointermove', onPointerMove);
142
- scrollArea.removeEventListener('pointerup', onPointerUp);
143
- }
144
- };
145
- }, [animateScroll, onScaleChange, scrollAreaRef]);
146
-
147
- return isPointerZooming;
148
- }
@@ -1,50 +0,0 @@
1
- import React from 'react';
2
-
3
- import type { IconButtonProps, LightboxProps, SlideshowProps, ThumbnailProps } from '@lumx/react';
4
- import type { HasClassName } from '@lumx/react/utils/type';
5
- import type { ImageCaptionMetadata } from '@lumx/react/components/image-block/ImageCaption';
6
-
7
- export type InheritedSlideShowProps = Pick<SlideshowProps, 'slideshowControlsProps' | 'slideGroupLabel'>;
8
-
9
- export interface ZoomButtonProps {
10
- /** Zoom in button props */
11
- zoomInButtonProps?: IconButtonProps;
12
- /** Zoom out button props */
13
- zoomOutButtonProps?: IconButtonProps;
14
- }
15
-
16
- export type InheritedThumbnailProps = Pick<
17
- ThumbnailProps,
18
- 'image' | 'alt' | 'imgProps' | 'imgRef' | 'loadingPlaceholderImageRef'
19
- >;
20
-
21
- export type InheritedImageMetadata = Pick<ImageCaptionMetadata, 'title' | 'description' | 'tags'>;
22
-
23
- export type ImageProps = InheritedThumbnailProps & InheritedImageMetadata;
24
-
25
- export interface ImagesProps {
26
- /** Index of the active image to show on open */
27
- activeImageIndex?: number;
28
- /** List of images to display */
29
- images: Array<ImageProps>;
30
- /** Ref of the active image when the lightbox is open */
31
- activeImageRef?: React.Ref<HTMLImageElement>;
32
- }
33
-
34
- export type InheritedLightboxProps = Pick<
35
- LightboxProps,
36
- 'isOpen' | 'parentElement' | 'onClose' | 'closeButtonProps' | 'aria-label' | 'aria-labelledby'
37
- >;
38
-
39
- export type ForwardedProps = React.ComponentPropsWithoutRef<'div'>;
40
-
41
- /**
42
- * ImageLightbox component props
43
- */
44
- export interface ImageLightboxProps
45
- extends HasClassName,
46
- ZoomButtonProps,
47
- ImagesProps,
48
- InheritedSlideShowProps,
49
- InheritedLightboxProps,
50
- ForwardedProps {}
@@ -1,130 +0,0 @@
1
- import React from 'react';
2
-
3
- import memoize from 'lodash/memoize';
4
-
5
- import { startViewTransition } from '@lumx/react/utils/DOM/startViewTransition';
6
- import { findImage } from '@lumx/react/utils/DOM/findImage';
7
-
8
- import type { ImageLightboxProps } from './types';
9
- import { CLASSNAME } from './constants';
10
-
11
- /** Subset of the ImageLightboxProps managed by the useImageLightbox hook */
12
- type ManagedProps = Pick<
13
- ImageLightboxProps,
14
- 'isOpen' | 'images' | 'parentElement' | 'activeImageRef' | 'onClose' | 'activeImageIndex'
15
- >;
16
-
17
- const EMPTY_PROPS: ManagedProps = { isOpen: false, images: [], parentElement: React.createRef() };
18
-
19
- type TriggerOptions = Pick<ImageLightboxProps, 'activeImageIndex'>;
20
-
21
- /**
22
- * Set up an ImageLightbox with images and triggers.
23
- *
24
- * - Associate a trigger with the lightbox to properly focus the trigger on close
25
- * - Associate a trigger with an image to display on open
26
- * - Automatically provide a view transition between an image trigger and the displayed image on open & close
27
- *
28
- * @param initialProps Images to display in the image lightbox
29
- */
30
- export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
31
- initialProps: P,
32
- ): {
33
- /**
34
- * Generates trigger props
35
- * @param index Provide an index to choose which image to display when the image lightbox opens.
36
- * */
37
- getTriggerProps: (options?: TriggerOptions) => { onClick: React.MouseEventHandler; ref: React.Ref<any> };
38
- /** Props to forward to the ImageLightbox */
39
- imageLightboxProps: ManagedProps & P;
40
- } {
41
- const { images = [], ...otherProps } = initialProps;
42
-
43
- const imagesPropsRef = React.useRef(images);
44
- React.useEffect(() => {
45
- imagesPropsRef.current = images.map((props) => ({ imgRef: React.createRef(), ...props }));
46
- }, [images]);
47
-
48
- const currentImageRef = React.useRef<HTMLImageElement>(null);
49
- const [imageLightboxProps, setImageLightboxProps] = React.useState(
50
- () => ({ ...EMPTY_PROPS, ...otherProps }) as ManagedProps & P,
51
- );
52
-
53
- const getTriggerProps = React.useMemo(() => {
54
- const triggerImageRefs: Record<number, React.RefObject<HTMLImageElement>> = {};
55
-
56
- async function close() {
57
- const currentImage = currentImageRef.current;
58
- if (!currentImage) {
59
- return;
60
- }
61
- const currentIndex = imagesPropsRef.current.findIndex(
62
- ({ imgRef }) => (imgRef as any)?.current === currentImage,
63
- );
64
-
65
- await startViewTransition({
66
- changes() {
67
- // Close lightbox
68
- setImageLightboxProps((prevProps) => ({ ...prevProps, isOpen: false }));
69
- },
70
- // Morph from the image in lightbox to the image in trigger
71
- viewTransitionName: {
72
- source: currentImageRef,
73
- target: triggerImageRefs[currentIndex],
74
- name: CLASSNAME,
75
- },
76
- });
77
- }
78
-
79
- async function open(triggerElement: HTMLElement, { activeImageIndex }: TriggerOptions = {}) {
80
- // If we find an image inside the trigger, animate it in transition with the opening image
81
- const triggerImage = triggerImageRefs[activeImageIndex as any]?.current || findImage(triggerElement);
82
-
83
- // Inject the trigger image as loading placeholder for better loading state
84
- const imagesWithFallbackSize = imagesPropsRef.current.map((image, idx) => {
85
- if (triggerImage && idx === activeImageIndex && !image.loadingPlaceholderImageRef) {
86
- return { ...image, loadingPlaceholderImageRef: { current: triggerImage } };
87
- }
88
- return image;
89
- });
90
-
91
- await startViewTransition({
92
- changes: () => {
93
- // Open lightbox with setup props
94
- setImageLightboxProps((prevProps) => ({
95
- ...prevProps,
96
- activeImageRef: currentImageRef,
97
- parentElement: { current: triggerElement },
98
- isOpen: true,
99
- onClose: () => {
100
- close();
101
- prevProps?.onClose?.();
102
- },
103
- images: imagesWithFallbackSize,
104
- activeImageIndex: activeImageIndex || 0,
105
- }));
106
- },
107
- // Morph from the image in trigger to the image in lightbox
108
- viewTransitionName: {
109
- source: triggerImage,
110
- target: currentImageRef,
111
- name: CLASSNAME,
112
- },
113
- });
114
- }
115
-
116
- return memoize((options?: TriggerOptions) => ({
117
- ref(element: HTMLElement | null) {
118
- // Track trigger image ref if any
119
- if (options?.activeImageIndex !== undefined && element) {
120
- triggerImageRefs[options.activeImageIndex] = { current: findImage(element) };
121
- }
122
- },
123
- onClick(e: React.MouseEvent) {
124
- open(e.target as HTMLElement, options);
125
- },
126
- }));
127
- }, []);
128
-
129
- return { getTriggerProps, imageLightboxProps };
130
- }
@@ -1,32 +0,0 @@
1
- import React from 'react';
2
-
3
- import throttle from 'lodash/throttle';
4
- import { RectSize } from '@lumx/react/utils/type';
5
-
6
- /**
7
- * Observe element size (only works if it's size depends on the window size).
8
- *
9
- * (Not using ResizeObserver for better browser backward compat)
10
- *
11
- * @param elementRef Element to observe
12
- * @return the size and a manual update callback
13
- */
14
- export function useElementSizeDependentOfWindowSize(
15
- elementRef: React.RefObject<HTMLElement>,
16
- ): [RectSize | null, () => void] {
17
- const [size, setSize] = React.useState<null | RectSize>(null);
18
- const updateSize = React.useMemo(
19
- () =>
20
- throttle(() => {
21
- const newSize = elementRef.current?.getBoundingClientRect();
22
- if (newSize) setSize(newSize);
23
- }, 10),
24
- [elementRef],
25
- );
26
- React.useEffect(() => {
27
- updateSize();
28
- window.addEventListener('resize', updateSize);
29
- return () => window.removeEventListener('resize', updateSize);
30
- }, [updateSize]);
31
- return [size, updateSize];
32
- }
@@ -1,17 +0,0 @@
1
- import React from 'react';
2
- import { RectSize } from '@lumx/react/utils/type';
3
-
4
- /** Get natural image size after load. */
5
- export function useImageSize(imgRef: React.RefObject<HTMLImageElement>, getInitialSize?: () => RectSize | null) {
6
- const [imageSize, setImageSize] = React.useState<null | RectSize>(getInitialSize || null);
7
- React.useEffect(() => {
8
- const { current: img } = imgRef;
9
- if (!img) {
10
- return undefined;
11
- }
12
- const onLoad = () => setImageSize({ width: img.naturalWidth, height: img.naturalHeight });
13
- img.addEventListener('load', onLoad);
14
- return () => img.removeEventListener('load', onLoad);
15
- }, [imgRef]);
16
- return imageSize;
17
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * File generated when storybook is started. Do not edit directly!
3
- */
4
- export default { title: 'LumX components/image-lightbox/ImageLightbox Demos' };
5
-
6
- export { App as Default } from './default';
@@ -1,3 +0,0 @@
1
- /** Find image in element including the element */
2
- export const findImage = (element: HTMLElement | null): HTMLImageElement | null =>
3
- element?.matches('img') ? (element as HTMLImageElement) : element?.querySelector('img') || null;
@@ -1,56 +0,0 @@
1
- import ReactDOM from 'react-dom';
2
-
3
- import { MaybeElementOrRef } from '@lumx/react/utils/type';
4
-
5
- import { unref } from '../react/unref';
6
- import { getPrefersReducedMotion } from '../browser/getPrefersReducedMotion';
7
-
8
- function setTransitionViewName(elementRef: MaybeElementOrRef<HTMLElement>, name: string | null | undefined) {
9
- const element = unref(elementRef) as any;
10
- if (element) element.style.viewTransitionName = name;
11
- }
12
-
13
- /**
14
- * Wrapper around the `document.startViewTransition` handling browser incompatibilities, react DOM flush and
15
- * user preference.
16
- *
17
- * @param changes callback containing the changes to apply within the view transition.
18
- * @param setViewTransitionName set the `viewTransitionName` style on a `source` & `target` to morph these elements.
19
- */
20
- export async function startViewTransition({
21
- changes,
22
- viewTransitionName,
23
- }: {
24
- changes: () => void;
25
- viewTransitionName: {
26
- source: MaybeElementOrRef<HTMLElement>;
27
- target: MaybeElementOrRef<HTMLElement>;
28
- name: string;
29
- };
30
- }) {
31
- const start = (document as any)?.startViewTransition?.bind(document);
32
- const prefersReducedMotion = getPrefersReducedMotion();
33
- const { flushSync } = ReactDOM as any;
34
- if (prefersReducedMotion || !start || !flushSync || !viewTransitionName?.source || !viewTransitionName?.target) {
35
- // Skip, apply changes without a transition
36
- changes();
37
- return;
38
- }
39
-
40
- // Set transition name on source element
41
- setTransitionViewName(viewTransitionName.source, viewTransitionName.name);
42
-
43
- // Start view transition, apply changes & flush to DOM
44
- await start(() => {
45
- // Un-set transition name on source element
46
- setTransitionViewName(viewTransitionName.source, null);
47
-
48
- flushSync(changes);
49
-
50
- // Set transition name on target element
51
- setTransitionViewName(viewTransitionName.target, viewTransitionName.name);
52
- }).updateCallbackDone;
53
-
54
- // Un-set transition name on target element
55
- setTransitionViewName(viewTransitionName.target, null);
56
- }
@@ -1,6 +0,0 @@
1
- import { WINDOW } from '@lumx/react/constants';
2
-
3
- /** Check if user prefers reduced motion */
4
- export function getPrefersReducedMotion() {
5
- return WINDOW?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
6
- }
@@ -1,25 +0,0 @@
1
- import { isEqual } from './isEqual';
2
-
3
- test(isEqual.name, () => {
4
- expect(isEqual('', '')).toBe(true);
5
- expect(isEqual(0, 0)).toBe(true);
6
- expect(isEqual(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY)).toBe(true);
7
-
8
- expect(isEqual('', 0)).toBe(false);
9
-
10
- expect(isEqual({}, {})).toBe(true);
11
- expect(isEqual({ a: 1 }, { a: 1 })).toBe(true);
12
- expect(isEqual({ a: { a: 1 } }, { a: { a: 1 } })).toBe(true);
13
-
14
- expect(isEqual([], [])).toBe(true);
15
-
16
- expect(isEqual([1], [2])).toBe(false);
17
- expect(isEqual([1], [1, 2])).toBe(false);
18
- expect(isEqual([1, 2], [2, 1])).toBe(false);
19
-
20
- expect(isEqual({ a: 1 }, { a: 2 })).toBe(false);
21
- expect(isEqual({ a: 1 }, {})).toBe(false);
22
- expect(isEqual({}, { a: 1 })).toBe(false);
23
- expect(isEqual({ a: { a: 1 } }, { a: { a: 2 } })).toBe(false);
24
- expect(isEqual({ a: 1 }, { a: 1, b: 1 })).toBe(false);
25
- });
@@ -1,11 +0,0 @@
1
- /** Minimal recursive deep equal of JS values */
2
- export function isEqual(obj1: any, obj2: any): boolean {
3
- if (obj1 === obj2) return true;
4
- if (typeof obj1 === 'object' && typeof obj2 === 'object') {
5
- const keys1 = Object.keys(obj1);
6
- const keys2 = Object.keys(obj2);
7
- if (keys1.length !== keys2.length) return false;
8
- return keys1.every((key1) => isEqual(obj1[key1], obj2[key1]));
9
- }
10
- return false;
11
- }
@@ -1,7 +0,0 @@
1
- import { MaybeElementOrRef } from '@lumx/react/utils/type';
2
-
3
- /** Unref a react ref or element */
4
- export function unref(maybeElement: MaybeElementOrRef<HTMLElement>) {
5
- if (maybeElement instanceof HTMLElement) return maybeElement;
6
- return maybeElement?.current;
7
- }
File without changes