@lumx/react 3.7.5 → 3.7.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/index.d.ts +76 -12
  2. package/index.js +1466 -718
  3. package/index.js.map +1 -1
  4. package/package.json +3 -3
  5. package/src/components/image-block/ImageBlock.tsx +13 -42
  6. package/src/components/image-block/ImageCaption.tsx +73 -0
  7. package/src/components/image-block/constants.ts +11 -0
  8. package/src/components/image-lightbox/ImageLightbox.stories.tsx +163 -0
  9. package/src/components/image-lightbox/ImageLightbox.test.tsx +252 -0
  10. package/src/components/image-lightbox/ImageLightbox.tsx +72 -0
  11. package/src/components/image-lightbox/constants.ts +11 -0
  12. package/src/components/image-lightbox/index.ts +2 -0
  13. package/src/components/image-lightbox/internal/ImageSlide.tsx +99 -0
  14. package/src/components/image-lightbox/internal/ImageSlideshow.tsx +158 -0
  15. package/src/components/image-lightbox/internal/useAnimateScroll.ts +55 -0
  16. package/src/components/image-lightbox/internal/usePointerZoom.ts +148 -0
  17. package/src/components/image-lightbox/types.ts +49 -0
  18. package/src/components/image-lightbox/useImageLightbox.tsx +122 -0
  19. package/src/components/lightbox/Lightbox.tsx +13 -12
  20. package/src/components/thumbnail/useFocusPointStyle.tsx +3 -4
  21. package/src/hooks/useElementSizeDependentOfWindowSize.ts +32 -0
  22. package/src/hooks/useImageSize.ts +17 -0
  23. package/src/index.ts +1 -0
  24. package/src/utils/findImage.tsx +3 -0
  25. package/src/utils/getPrefersReducedMotion.ts +6 -0
  26. package/src/utils/startViewTransition.ts +54 -0
  27. package/src/utils/type.ts +15 -0
  28. package/src/utils/unref.ts +6 -0
  29. package/src/hooks/useOnResize.ts +0 -41
@@ -4,7 +4,7 @@ import classNames from 'classnames';
4
4
  import { createPortal } from 'react-dom';
5
5
 
6
6
  import { mdiClose } from '@lumx/icons';
7
- import { ColorPalette, Emphasis, IconButton, IconButtonProps } from '@lumx/react';
7
+ import { IconButton, IconButtonProps } from '@lumx/react';
8
8
  import { DIALOG_TRANSITION_DURATION, DOCUMENT } from '@lumx/react/constants';
9
9
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
10
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
@@ -145,17 +145,18 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
145
145
  style={{ zIndex }}
146
146
  >
147
147
  {closeButtonProps && (
148
- <IconButton
149
- {...closeButtonProps}
150
- ref={closeButtonRef}
151
- className={`${CLASSNAME}__close`}
152
- color={ColorPalette.light}
153
- emphasis={Emphasis.low}
154
- icon={mdiClose}
155
- theme={theme}
156
- type="button"
157
- onClick={onClose}
158
- />
148
+ <div className={`${CLASSNAME}__close`}>
149
+ <IconButton
150
+ {...closeButtonProps}
151
+ ref={closeButtonRef}
152
+ emphasis="low"
153
+ hasBackground
154
+ icon={mdiClose}
155
+ theme="dark"
156
+ type="button"
157
+ onClick={onClose}
158
+ />
159
+ </div>
159
160
  )}
160
161
  <ClickAwayProvider callback={!preventAutoClose && onClose} childrenRefs={clickAwayRefs}>
161
162
  <div ref={childrenRef} className={`${CLASSNAME}__wrapper`} role="presentation">
@@ -1,6 +1,7 @@
1
1
  import { CSSProperties, useEffect, useMemo, useState } from 'react';
2
2
  import { AspectRatio } from '@lumx/react/components';
3
3
  import { ThumbnailProps } from '@lumx/react/components/thumbnail/Thumbnail';
4
+ import { RectSize } from '@lumx/react/utils/type';
4
5
 
5
6
  // Calculate shift to center the focus point in the container.
6
7
  export function shiftPosition({
@@ -24,8 +25,6 @@ export function shiftPosition({
24
25
  return Math.floor(Math.max(Math.min(shift, 1), 0) * 100);
25
26
  }
26
27
 
27
- type Size = { width: number; height: number };
28
-
29
28
  // Compute CSS properties to apply the focus point.
30
29
  export const useFocusPointStyle = (
31
30
  { image, aspectRatio, focusPoint, imgProps: { width, height } = {} }: ThumbnailProps,
@@ -33,7 +32,7 @@ export const useFocusPointStyle = (
33
32
  isLoaded: boolean,
34
33
  ): CSSProperties => {
35
34
  // Get natural image size from imgProps or img element.
36
- const imageSize: Size | undefined = useMemo(() => {
35
+ const imageSize: RectSize | undefined = useMemo(() => {
37
36
  // Focus point is not applicable => exit early
38
37
  if (!image || aspectRatio === AspectRatio.original || (!focusPoint?.x && !focusPoint?.y)) return undefined;
39
38
  if (typeof width === 'number' && typeof height === 'number') return { width, height };
@@ -42,7 +41,7 @@ export const useFocusPointStyle = (
42
41
  }, [aspectRatio, element, focusPoint?.x, focusPoint?.y, height, image, isLoaded, width]);
43
42
 
44
43
  // Get container size (dependant on imageSize).
45
- const [containerSize, setContainerSize] = useState<Size | undefined>(undefined);
44
+ const [containerSize, setContainerSize] = useState<RectSize | undefined>(undefined);
46
45
  useEffect(
47
46
  function updateContainerSize() {
48
47
  const cWidth = element?.offsetWidth;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+
3
+ import { RectSize } from '@lumx/react/utils/type';
4
+ import throttle from 'lodash/throttle';
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
+ }
@@ -0,0 +1,17 @@
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
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from './components/grid';
25
25
  export * from './components/grid-column';
26
26
  export * from './components/icon';
27
27
  export * from './components/image-block';
28
+ export * from './components/image-lightbox';
28
29
  export * from './components/inline-list';
29
30
  export * from './components/input-helper';
30
31
  export * from './components/input-label';
@@ -0,0 +1,3 @@
1
+ /** Find image in element including the element */
2
+ export const findImage = (element: HTMLElement | null) =>
3
+ element?.matches('img') ? (element as HTMLImageElement) : element?.querySelector('img');
@@ -0,0 +1,6 @@
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
+ }
@@ -0,0 +1,54 @@
1
+ import ReactDOM from 'react-dom';
2
+ import { getPrefersReducedMotion } from '@lumx/react/utils/getPrefersReducedMotion';
3
+ import { MaybeElementOrRef } from '@lumx/react/utils/type';
4
+ import { unref } from '@lumx/react/utils/unref';
5
+
6
+ function setTransitionViewName(elementRef: MaybeElementOrRef<HTMLElement>, name: string | null | undefined) {
7
+ const element = unref(elementRef) as any;
8
+ if (element) element.style.viewTransitionName = name;
9
+ }
10
+
11
+ /**
12
+ * Wrapper around the `document.startViewTransition` handling browser incompatibilities, react DOM flush and
13
+ * user preference.
14
+ *
15
+ * @param changes callback containing the changes to apply within the view transition.
16
+ * @param setViewTransitionName set the `viewTransitionName` style on a `source` & `target` to morph these elements.
17
+ */
18
+ export async function startViewTransition({
19
+ changes,
20
+ viewTransitionName,
21
+ }: {
22
+ changes: () => void;
23
+ viewTransitionName: {
24
+ source: MaybeElementOrRef<HTMLElement>;
25
+ target: MaybeElementOrRef<HTMLElement>;
26
+ name: string;
27
+ };
28
+ }) {
29
+ const start = (document as any)?.startViewTransition?.bind(document);
30
+ const prefersReducedMotion = getPrefersReducedMotion();
31
+ const { flushSync } = ReactDOM as any;
32
+ if (prefersReducedMotion || !start || !flushSync || !viewTransitionName?.source || !viewTransitionName?.target) {
33
+ // Skip, apply changes without a transition
34
+ changes();
35
+ return;
36
+ }
37
+
38
+ // Set transition name on source element
39
+ setTransitionViewName(viewTransitionName.source, viewTransitionName.name);
40
+
41
+ // Start view transition, apply changes & flush to DOM
42
+ await start(() => {
43
+ // Un-set transition name on source element
44
+ setTransitionViewName(viewTransitionName.source, null);
45
+
46
+ flushSync(changes);
47
+
48
+ // Set transition name on target element
49
+ setTransitionViewName(viewTransitionName.target, viewTransitionName.name);
50
+ }).updateCallbackDone;
51
+
52
+ // Un-set transition name on target element
53
+ setTransitionViewName(viewTransitionName.target, null);
54
+ }
package/src/utils/type.ts CHANGED
@@ -139,3 +139,18 @@ export type ComponentRef<C> = C extends keyof JSX.IntrinsicElements
139
139
  : C extends React.JSXElementConstructor<{ ref?: infer R }>
140
140
  ? R
141
141
  : never;
142
+
143
+ /**
144
+ * Rectangle size
145
+ */
146
+ export type RectSize = { width: number; height: number };
147
+
148
+ /**
149
+ * Maybe a HTMLElement or a React ref of a HTMLElement
150
+ */
151
+ export type MaybeElementOrRef<E extends HTMLElement> = E | React.RefObject<E | null> | null | undefined;
152
+
153
+ /**
154
+ * A point coordinate in 2D space
155
+ */
156
+ export type Point = { x: number; y: number };
@@ -0,0 +1,6 @@
1
+ import { MaybeElementOrRef } from '@lumx/react/utils/type';
2
+
3
+ export function unref(maybeElement: MaybeElementOrRef<HTMLElement>) {
4
+ if (maybeElement instanceof HTMLElement) return maybeElement;
5
+ return maybeElement?.current;
6
+ }
@@ -1,41 +0,0 @@
1
- import { Callback, Falsy } from '@lumx/react/utils/type';
2
- import { MutableRefObject, RefObject, useEffect, useRef } from 'react';
3
- import { WINDOW } from '@lumx/react/constants';
4
- import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
5
-
6
- const ResizeObserver: typeof Polyfill = (WINDOW as any)?.ResizeObserver || Polyfill;
7
-
8
- export function useOnResize(element: HTMLElement | Falsy, update: RefObject<Callback>): void {
9
- const observerRef = useRef(null) as MutableRefObject<Polyfill | null>;
10
- const previousSize = useRef<{ width: number; height: number }>();
11
-
12
- useEffect(() => {
13
- if (!element || !update) {
14
- return undefined;
15
- }
16
-
17
- previousSize.current = undefined;
18
- const observer =
19
- observerRef.current ||
20
- new ResizeObserver(([entry]) => {
21
- const updateFunction = update.current;
22
- if (!updateFunction) {
23
- return;
24
- }
25
-
26
- const { width, height } = entry.contentRect;
27
- if (previousSize.current?.width === width && previousSize.current?.height === height) {
28
- return;
29
- }
30
-
31
- window.requestAnimationFrame(() => updateFunction());
32
- previousSize.current = entry.contentRect;
33
- });
34
- if (!observerRef.current) observerRef.current = observer;
35
-
36
- observer.observe(element);
37
- return () => {
38
- observer.unobserve(element);
39
- };
40
- }, [element, update]);
41
- }