@expofp/ui 3.23.0 → 3.23.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.
- package/dist/components/Gallery/Gallery.d.ts +24 -0
- package/dist/components/Gallery/Gallery.js +28 -0
- package/dist/components/Gallery/GalleryBadges/GalleryBadges.d.ts +8 -0
- package/dist/components/Gallery/GalleryBadges/GalleryBadges.js +5 -0
- package/dist/components/Gallery/GalleryControls/GalleryControls.d.ts +11 -0
- package/dist/components/Gallery/GalleryControls/GalleryControls.js +5 -0
- package/dist/components/Gallery/GalleryImg/GalleryImg.d.ts +13 -0
- package/dist/components/Gallery/GalleryImg/GalleryImg.js +37 -0
- package/dist/components/Gallery/GalleryItem/GalleryItem.d.ts +14 -0
- package/dist/components/Gallery/GalleryItem/GalleryItem.js +6 -0
- package/dist/components/Gallery/GalleryModal/GalleryModal.d.ts +20 -0
- package/dist/components/Gallery/GalleryModal/GalleryModal.js +90 -0
- package/dist/components/Gallery/GalleryPreLoader.d.ts +7 -0
- package/dist/components/Gallery/GalleryPreLoader.js +29 -0
- package/dist/components/Gallery/TransformImg/TransformImg.d.ts +10 -0
- package/dist/components/Gallery/TransformImg/TransformImg.js +15 -0
- package/dist/components/Gallery/index.d.ts +4 -0
- package/dist/components/Gallery/index.js +1 -0
- package/dist/components/Gallery/types.d.ts +6 -0
- package/dist/components/Gallery/types.js +1 -0
- package/dist/components/Modal/Modal.js +10 -6
- package/dist/components/Overlay/Overlay.d.ts +29 -0
- package/dist/components/Overlay/Overlay.js +251 -0
- package/dist/components/Overlay/OverlayScrollContext.d.ts +7 -0
- package/dist/components/Overlay/OverlayScrollContext.js +7 -0
- package/dist/components/Overlay/index.d.ts +4 -0
- package/dist/components/Overlay/index.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/shared/activeElement.d.ts +7 -0
- package/dist/shared/activeElement.js +14 -0
- package/dist/shared/debounce.d.ts +2 -0
- package/dist/shared/debounce.js +9 -0
- package/dist/shared/hooks/useDragGesture.d.ts +19 -0
- package/dist/shared/hooks/useDragGesture.js +45 -0
- package/dist/shared/hooks/useWindowSize.d.ts +5 -0
- package/dist/shared/hooks/useWindowSize.js +40 -0
- package/package.json +3 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import './Gallery.scss';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { GalleryImage } from './types';
|
|
4
|
+
export interface GalleryProps {
|
|
5
|
+
images: GalleryImage[];
|
|
6
|
+
leading?: boolean;
|
|
7
|
+
closeLabel: string;
|
|
8
|
+
zoomInLabel: string;
|
|
9
|
+
zoomOutLabel: string;
|
|
10
|
+
nextSlideLabel: string;
|
|
11
|
+
prevSlideLabel: string;
|
|
12
|
+
fullscreenLabel: string;
|
|
13
|
+
itemLabel: (index: number) => string;
|
|
14
|
+
renderTarget?: HTMLElement | null;
|
|
15
|
+
onImageLoadHeightUpdate?: () => void;
|
|
16
|
+
/** Fired with the index of the slide shown fullscreen, on open and on slide change. */
|
|
17
|
+
onSlideActivate?: (index: number) => void;
|
|
18
|
+
onOpenGallery?: () => void;
|
|
19
|
+
onCloseGallery?: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const Gallery: (props: GalleryProps) => React.JSX.Element;
|
|
23
|
+
export default Gallery;
|
|
24
|
+
//# sourceMappingURL=Gallery.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import './Gallery.scss';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
import GalleryBadges from './GalleryBadges/GalleryBadges';
|
|
6
|
+
import GalleryItem from './GalleryItem/GalleryItem';
|
|
7
|
+
import GalleryModal from './GalleryModal/GalleryModal';
|
|
8
|
+
const Gallery = (props) => {
|
|
9
|
+
const { images, leading = false, closeLabel, zoomInLabel, zoomOutLabel, nextSlideLabel, prevSlideLabel, fullscreenLabel, itemLabel, renderTarget, onImageLoadHeightUpdate, onSlideActivate, onOpenGallery, onCloseGallery, className, } = props;
|
|
10
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
11
|
+
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
|
|
12
|
+
const openModal = (initialSlideIndex) => {
|
|
13
|
+
// No portal target, no modal — and no opening to report either.
|
|
14
|
+
if (!renderTarget)
|
|
15
|
+
return;
|
|
16
|
+
setCurrentSlideIndex(initialSlideIndex);
|
|
17
|
+
setIsModalOpen(true);
|
|
18
|
+
onOpenGallery?.();
|
|
19
|
+
};
|
|
20
|
+
const closeModal = () => {
|
|
21
|
+
setIsModalOpen(false);
|
|
22
|
+
onCloseGallery?.();
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("div", { className: classNames('efp-gallery', {
|
|
25
|
+
'efp-gallery--leading': leading,
|
|
26
|
+
}, className), children: [_jsx("div", { className: "efp-gallery__wrapper", children: leading ? (_jsx(GalleryItem, { url: images[0].thumbnailUrl, index: 0, itemLabel: itemLabel, leading: leading, autoHeight: true, fillMode: "contain", onClick: () => openModal(0), onImageLoadHeightUpdate: onImageLoadHeightUpdate })) : (images.map((image, i) => (_jsx(GalleryItem, { url: image.thumbnailUrl, index: i, itemLabel: itemLabel, position: "top", fillMode: "cover", onClick: () => openModal(i), onImageLoadHeightUpdate: onImageLoadHeightUpdate }, image.thumbnailUrl + i)))) }), !leading && (_jsx(GalleryBadges, { fullscreenLabel: fullscreenLabel, onFullscreen: () => openModal(0), count: images.length }))] }), isModalOpen && renderTarget && (_jsx(GalleryModal, { className: className, images: images, leading: leading, initialSlideIndex: currentSlideIndex, closeLabel: closeLabel, zoomInLabel: zoomInLabel, zoomOutLabel: zoomOutLabel, nextSlideLabel: nextSlideLabel, prevSlideLabel: prevSlideLabel, itemLabel: itemLabel, renderTarget: renderTarget, onSlideActivate: onSlideActivate, onClose: closeModal }))] }));
|
|
27
|
+
};
|
|
28
|
+
export default Gallery;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GalleryBadgesProps {
|
|
2
|
+
count: number;
|
|
3
|
+
fullscreenLabel: string;
|
|
4
|
+
onFullscreen: () => void;
|
|
5
|
+
}
|
|
6
|
+
declare const GalleryBadges: ({ count, fullscreenLabel, onFullscreen }: GalleryBadgesProps) => import("react").JSX.Element;
|
|
7
|
+
export default GalleryBadges;
|
|
8
|
+
//# sourceMappingURL=GalleryBadges.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const GalleryBadges = ({ count, fullscreenLabel, onFullscreen }) => {
|
|
3
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "efp-gallery__badge efp-gallery__badge-count", children: [_jsx("i", { className: "icon-image", "aria-hidden": "true" }), _jsx("span", { children: count })] }), _jsx("button", { type: "button", className: "efp-gallery__badge efp-gallery__badge-fullscreen", "aria-label": fullscreenLabel, title: fullscreenLabel, onClick: onFullscreen, children: _jsx("i", { className: "icon-maximize", "aria-hidden": "true" }) })] }));
|
|
4
|
+
};
|
|
5
|
+
export default GalleryBadges;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface GalleryControlsProps {
|
|
2
|
+
closeLabel: string;
|
|
3
|
+
zoomInLabel: string;
|
|
4
|
+
zoomOutLabel: string;
|
|
5
|
+
zoomIn: () => void;
|
|
6
|
+
zoomOut: () => void;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
declare const GalleryControls: ({ closeLabel, zoomInLabel, zoomOutLabel, zoomIn, zoomOut, onClose, }: GalleryControlsProps) => import("react").JSX.Element;
|
|
10
|
+
export default GalleryControls;
|
|
11
|
+
//# sourceMappingURL=GalleryControls.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const GalleryControls = ({ closeLabel, zoomInLabel, zoomOutLabel, zoomIn, zoomOut, onClose, }) => {
|
|
3
|
+
return (_jsxs("div", { className: "efp-gallery-slider__controls", children: [_jsx("button", { type: "button", className: "efp-gallery-slider__btn efp-gallery-slider__btn--close", title: closeLabel, "aria-label": closeLabel, onClick: onClose, children: _jsx("i", { className: "icon-close", "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "efp-gallery-slider__btn efp-gallery-slider__btn--zoom-in", title: zoomInLabel, "aria-label": zoomInLabel, onClick: () => zoomIn(), children: _jsx("i", { className: "icon-zoom-in", "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "efp-gallery-slider__btn efp-gallery-slider__btn--zoom-out", title: zoomOutLabel, "aria-label": zoomOutLabel, onClick: () => zoomOut(), children: _jsx("i", { className: "icon-zoom-out", "aria-hidden": "true" }) })] }));
|
|
4
|
+
};
|
|
5
|
+
export default GalleryControls;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type FillMode = 'cover' | 'contain';
|
|
2
|
+
export interface GalleryImgProps {
|
|
3
|
+
url: string;
|
|
4
|
+
fillMode?: FillMode;
|
|
5
|
+
leading?: boolean;
|
|
6
|
+
autoHeight?: boolean;
|
|
7
|
+
fullscreen?: boolean;
|
|
8
|
+
position?: 'center' | 'top';
|
|
9
|
+
onImageLoadHeightUpdate?: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const GalleryImg: ({ url, autoHeight, position, fullscreen, leading, fillMode, onImageLoadHeightUpdate, }: GalleryImgProps) => import("react").JSX.Element;
|
|
12
|
+
export default GalleryImg;
|
|
13
|
+
//# sourceMappingURL=GalleryImg.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import GalleryPreLoader from '../GalleryPreLoader';
|
|
4
|
+
const GalleryImg = ({ url, autoHeight = false, position = 'center', fullscreen = false, leading = false, fillMode = 'contain', onImageLoadHeightUpdate, }) => {
|
|
5
|
+
const imgRef = useRef(null);
|
|
6
|
+
const containerRef = useRef(null);
|
|
7
|
+
const loadImage = useCallback(async (imageUrl) => {
|
|
8
|
+
if (!autoHeight || !containerRef.current)
|
|
9
|
+
return;
|
|
10
|
+
const container = containerRef.current;
|
|
11
|
+
try {
|
|
12
|
+
const loadedImage = await GalleryPreLoader.load(imageUrl);
|
|
13
|
+
if (!loadedImage)
|
|
14
|
+
return;
|
|
15
|
+
container.style.height =
|
|
16
|
+
(loadedImage.height * container.clientWidth) / loadedImage.width + 'px';
|
|
17
|
+
onImageLoadHeightUpdate?.();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// image failed to load — keep default height
|
|
21
|
+
}
|
|
22
|
+
}, [autoHeight, onImageLoadHeightUpdate]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!imgRef.current || !containerRef.current || !leading)
|
|
25
|
+
return;
|
|
26
|
+
void loadImage(url);
|
|
27
|
+
}, [url, leading, loadImage]);
|
|
28
|
+
const style = {
|
|
29
|
+
backgroundImage: `url("${url}")`,
|
|
30
|
+
backgroundSize: fillMode,
|
|
31
|
+
backgroundRepeat: 'no-repeat',
|
|
32
|
+
transition: leading ? 'all 0.5s ease 0s' : 'none',
|
|
33
|
+
backgroundPosition: position,
|
|
34
|
+
};
|
|
35
|
+
return (_jsx("div", { ref: containerRef, style: { width: '100%', height: leading && !fullscreen ? '250px' : '100%' }, children: _jsx("div", { ref: imgRef, className: "efp-gallery__img", style: style }) }));
|
|
36
|
+
};
|
|
37
|
+
export default GalleryImg;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GalleryItemProps {
|
|
2
|
+
url: string;
|
|
3
|
+
index: number;
|
|
4
|
+
itemLabel: (index: number) => string;
|
|
5
|
+
leading?: boolean;
|
|
6
|
+
autoHeight?: boolean;
|
|
7
|
+
fillMode?: 'cover' | 'contain';
|
|
8
|
+
position?: 'center' | 'top';
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
onImageLoadHeightUpdate?: () => void;
|
|
11
|
+
}
|
|
12
|
+
declare const GalleryItem: ({ url, index, itemLabel, position, leading, autoHeight, fillMode, onClick, onImageLoadHeightUpdate, }: GalleryItemProps) => import("react").JSX.Element;
|
|
13
|
+
export default GalleryItem;
|
|
14
|
+
//# sourceMappingURL=GalleryItem.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import GalleryImg from '../GalleryImg/GalleryImg';
|
|
3
|
+
const GalleryItem = ({ url, index, itemLabel, position = 'center', leading = false, autoHeight = false, fillMode, onClick, onImageLoadHeightUpdate, }) => {
|
|
4
|
+
return (_jsx("button", { type: "button", className: "efp-gallery__item", "aria-label": itemLabel(index), onClick: onClick, children: _jsx(GalleryImg, { position: position, fillMode: fillMode, url: url, leading: leading, autoHeight: autoHeight, onImageLoadHeightUpdate: onImageLoadHeightUpdate }) }));
|
|
5
|
+
};
|
|
6
|
+
export default GalleryItem;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import './GalleryModal.scss';
|
|
2
|
+
import type { GalleryImage } from '../types';
|
|
3
|
+
export interface GalleryModalProps {
|
|
4
|
+
images: GalleryImage[];
|
|
5
|
+
leading: boolean;
|
|
6
|
+
initialSlideIndex: number;
|
|
7
|
+
closeLabel: string;
|
|
8
|
+
zoomInLabel: string;
|
|
9
|
+
zoomOutLabel: string;
|
|
10
|
+
nextSlideLabel: string;
|
|
11
|
+
prevSlideLabel: string;
|
|
12
|
+
itemLabel: (index: number) => string;
|
|
13
|
+
renderTarget: HTMLElement;
|
|
14
|
+
onSlideActivate?: (index: number) => void;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const GalleryModal: (props: GalleryModalProps) => import("react").ReactPortal;
|
|
19
|
+
export default GalleryModal;
|
|
20
|
+
//# sourceMappingURL=GalleryModal.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import './GalleryModal.scss';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { TransformWrapper } from 'react-zoom-pan-pinch-sr';
|
|
7
|
+
import { Navigation } from 'swiper';
|
|
8
|
+
import { Swiper as SwiperComponent, SwiperSlide } from 'swiper/react';
|
|
9
|
+
import { getActiveElement } from '../../../shared/activeElement';
|
|
10
|
+
import useOnEscape from '../../../shared/hooks/useOnEscape';
|
|
11
|
+
import GalleryControls from '../GalleryControls/GalleryControls';
|
|
12
|
+
import TransformImg from '../TransformImg/TransformImg';
|
|
13
|
+
const isElementVisible = (el) => el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden';
|
|
14
|
+
const GalleryModal = (props) => {
|
|
15
|
+
const { images, leading, initialSlideIndex, closeLabel, zoomInLabel, zoomOutLabel, nextSlideLabel, prevSlideLabel, itemLabel, renderTarget, onSlideActivate, onClose, className, } = props;
|
|
16
|
+
const [currentSlideIndex, setCurrentSlideIndex] = useState(initialSlideIndex);
|
|
17
|
+
const [zoomUtils, setZoomUtils] = useState([]);
|
|
18
|
+
const swiperRef = useRef(null);
|
|
19
|
+
const prevRef = useRef(null);
|
|
20
|
+
const nextRef = useRef(null);
|
|
21
|
+
const modalRef = useRef(null);
|
|
22
|
+
useOnEscape(onClose);
|
|
23
|
+
// Scroll lock + focus trap/restore. Runs once for the modal's whole mounted
|
|
24
|
+
// lifetime — the parent only mounts this component while the modal is open.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const previousFocus = getActiveElement(modalRef.current);
|
|
27
|
+
const previousBodyOverflow = document.body.style.overflow;
|
|
28
|
+
document.body.style.overflow = 'hidden';
|
|
29
|
+
let inner = 0;
|
|
30
|
+
const outer = requestAnimationFrame(() => {
|
|
31
|
+
inner = requestAnimationFrame(() => modalRef.current?.focus());
|
|
32
|
+
});
|
|
33
|
+
const handleKeyDown = (e) => {
|
|
34
|
+
if (e.key !== 'Tab')
|
|
35
|
+
return;
|
|
36
|
+
const container = modalRef.current;
|
|
37
|
+
if (!container)
|
|
38
|
+
return;
|
|
39
|
+
const focusable = Array.from(container.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter(isElementVisible);
|
|
40
|
+
if (focusable.length === 0) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const first = focusable[0];
|
|
45
|
+
const last = focusable[focusable.length - 1];
|
|
46
|
+
const active = getActiveElement(container);
|
|
47
|
+
if (e.shiftKey) {
|
|
48
|
+
if (active === first || active === container) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
last.focus();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (active === last) {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
first.focus();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
59
|
+
return () => {
|
|
60
|
+
cancelAnimationFrame(outer);
|
|
61
|
+
cancelAnimationFrame(inner);
|
|
62
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
63
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
64
|
+
previousFocus?.focus();
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
onSlideActivate?.(currentSlideIndex);
|
|
69
|
+
}, [currentSlideIndex, onSlideActivate]);
|
|
70
|
+
const mainSliderOptions = {
|
|
71
|
+
initialSlide: initialSlideIndex,
|
|
72
|
+
draggable: false,
|
|
73
|
+
modules: [Navigation],
|
|
74
|
+
spaceBetween: 50,
|
|
75
|
+
slidesPerView: 1,
|
|
76
|
+
navigation: {
|
|
77
|
+
prevEl: prevRef.current,
|
|
78
|
+
nextEl: nextRef.current,
|
|
79
|
+
},
|
|
80
|
+
grabCursor: true,
|
|
81
|
+
};
|
|
82
|
+
const modalContent = (_jsx("div", { className: classNames('efp-gallery-modal', className), role: "dialog", "aria-modal": "true", "aria-label": itemLabel(currentSlideIndex), ref: modalRef, tabIndex: -1, children: _jsxs(SwiperComponent, { onSwiper: (swiper) => (swiperRef.current = swiper), className: "efp-gallery-slider", onSlideChange: (swiper) => {
|
|
83
|
+
setCurrentSlideIndex(swiper.activeIndex);
|
|
84
|
+
zoomUtils[swiper.previousIndex]?.resetTransform();
|
|
85
|
+
}, ...mainSliderOptions, children: [zoomUtils[currentSlideIndex] ? (_jsx(GalleryControls, { closeLabel: closeLabel, zoomInLabel: zoomInLabel, zoomOutLabel: zoomOutLabel, onClose: onClose, zoomIn: zoomUtils[currentSlideIndex].zoomIn, zoomOut: zoomUtils[currentSlideIndex].zoomOut })) : null, images.length > 1 && (_jsx("button", { type: "button", ref: nextRef, title: nextSlideLabel, "aria-label": nextSlideLabel, className: "efp-gallery-slider__btn efp-gallery-slider__btn--next", children: _jsx("i", { className: "icon-chevron-right", "aria-hidden": "true" }) })), images.length > 1 && (_jsx("button", { type: "button", ref: prevRef, title: prevSlideLabel, "aria-label": prevSlideLabel, className: "efp-gallery-slider__btn efp-gallery-slider__btn--prev", children: _jsx("i", { className: "icon-chevron-left", "aria-hidden": "true" }) })), images.map((image, i) => (_jsx(SwiperSlide, { children: _jsx(TransformWrapper, { initialScale: 1, alignmentAnimation: { sizeX: 0, sizeY: 0 }, onInit: (controls) => {
|
|
86
|
+
setZoomUtils((state) => [...state, controls]);
|
|
87
|
+
}, children: _jsx(TransformImg, { leading: leading, swiperRef: swiperRef, url: image.fullUrl }) }) }, image.thumbnailUrl + i)))] }) }));
|
|
88
|
+
return createPortal(modalContent, renderTarget);
|
|
89
|
+
};
|
|
90
|
+
export default GalleryModal;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class GalleryPreLoader {
|
|
2
|
+
cache = [];
|
|
3
|
+
load = (url) => {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
if (!url) {
|
|
6
|
+
resolve(null);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const cached = this.cache.find((i) => i.src === url);
|
|
10
|
+
if (cached) {
|
|
11
|
+
resolve(cached);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const image = new Image();
|
|
15
|
+
image.crossOrigin = 'anonymous';
|
|
16
|
+
image.src = url;
|
|
17
|
+
image.onload = () => {
|
|
18
|
+
if (this.cache.length > 10)
|
|
19
|
+
this.cache.shift();
|
|
20
|
+
this.cache.push(image);
|
|
21
|
+
resolve(image);
|
|
22
|
+
};
|
|
23
|
+
image.onerror = () => {
|
|
24
|
+
reject(new Error(`Failed to load image: ${url}`));
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export default new GalleryPreLoader();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type Swiper as SwiperInstance } from 'swiper';
|
|
3
|
+
export interface TransformImgProps {
|
|
4
|
+
swiperRef: React.RefObject<SwiperInstance | null>;
|
|
5
|
+
url: string;
|
|
6
|
+
leading: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const TransformImg: ({ swiperRef, leading, url }: TransformImgProps) => React.JSX.Element;
|
|
9
|
+
export default TransformImg;
|
|
10
|
+
//# sourceMappingURL=TransformImg.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { TransformComponent, useTransformEffect } from 'react-zoom-pan-pinch-sr';
|
|
3
|
+
import GalleryImg from '../GalleryImg/GalleryImg';
|
|
4
|
+
const TransformImg = ({ swiperRef, leading, url }) => {
|
|
5
|
+
useTransformEffect(({ state, instance }) => {
|
|
6
|
+
if (!swiperRef.current)
|
|
7
|
+
return;
|
|
8
|
+
swiperRef.current.allowTouchMove = state.scale === 1;
|
|
9
|
+
if (state.scale === 1 && state.positionX !== 0 && state.positionY !== 0) {
|
|
10
|
+
instance.setCenter();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return (_jsx(TransformComponent, { wrapperClass: "efp-gallery-slider__zoom", contentClass: "efp-gallery-slider__zoom-content", children: _jsx(GalleryImg, { fullscreen: true, leading: leading, url: url }) }));
|
|
14
|
+
};
|
|
15
|
+
export default TransformImg;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Gallery';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import './Modal.scss';
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
|
+
import { getActiveElement } from '../../shared/activeElement';
|
|
6
7
|
import Button from '../Button';
|
|
7
8
|
const MODAL_EXIT_TRANSITION_MS = 200;
|
|
8
9
|
const isElementVisible = (el) => el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden';
|
|
@@ -12,7 +13,8 @@ const Modal = ({ open, className, variant = 'default', title, ariaLabel, maxWidt
|
|
|
12
13
|
const titleId = useId();
|
|
13
14
|
const [isOpen, setIsOpen] = useState(open);
|
|
14
15
|
const [isVisible, setIsVisible] = useState(open);
|
|
15
|
-
// Focus restore + body scroll lock + enter/exit animation. Driven
|
|
16
|
+
// Focus restore + body scroll lock + enter/exit animation. Driven by `open`
|
|
17
|
+
// and by the portal target, without which nothing is rendered at all.
|
|
16
18
|
useEffect(() => {
|
|
17
19
|
if (!open) {
|
|
18
20
|
const deactivateFrame = requestAnimationFrame(() => setIsOpen(false));
|
|
@@ -24,7 +26,9 @@ const Modal = ({ open, className, variant = 'default', title, ariaLabel, maxWidt
|
|
|
24
26
|
clearTimeout(unmountTimer);
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
if (!renderTarget)
|
|
30
|
+
return;
|
|
31
|
+
previousFocusRef.current ??= getActiveElement(modalRef.current);
|
|
28
32
|
const previousBodyOverflow = document.body.style.overflow;
|
|
29
33
|
document.body.style.overflow = 'hidden';
|
|
30
34
|
let activateFrame = 0;
|
|
@@ -37,12 +41,12 @@ const Modal = ({ open, className, variant = 'default', title, ariaLabel, maxWidt
|
|
|
37
41
|
cancelAnimationFrame(activateFrame);
|
|
38
42
|
document.body.style.overflow = previousBodyOverflow;
|
|
39
43
|
};
|
|
40
|
-
}, [open]);
|
|
44
|
+
}, [open, renderTarget]);
|
|
41
45
|
// Keydown handling (Escape + focus trap). Kept separate from the lifecycle
|
|
42
46
|
// effect above so a new `onClose` identity only re-subscribes this listener
|
|
43
47
|
// and never restarts the enter/exit animation or scroll lock.
|
|
44
48
|
useEffect(() => {
|
|
45
|
-
if (!open)
|
|
49
|
+
if (!open || !renderTarget)
|
|
46
50
|
return;
|
|
47
51
|
const handleKeyDown = (e) => {
|
|
48
52
|
if (e.key === 'Escape') {
|
|
@@ -63,7 +67,7 @@ const Modal = ({ open, className, variant = 'default', title, ariaLabel, maxWidt
|
|
|
63
67
|
}
|
|
64
68
|
const first = focusable[0];
|
|
65
69
|
const last = focusable[focusable.length - 1];
|
|
66
|
-
const active =
|
|
70
|
+
const active = getActiveElement(container);
|
|
67
71
|
if (e.shiftKey) {
|
|
68
72
|
if (active === first || active === container) {
|
|
69
73
|
e.preventDefault();
|
|
@@ -77,7 +81,7 @@ const Modal = ({ open, className, variant = 'default', title, ariaLabel, maxWidt
|
|
|
77
81
|
};
|
|
78
82
|
document.addEventListener('keydown', handleKeyDown);
|
|
79
83
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
80
|
-
}, [open, onClose]);
|
|
84
|
+
}, [open, renderTarget, onClose]);
|
|
81
85
|
useEffect(() => {
|
|
82
86
|
if (!isOpen)
|
|
83
87
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import './Overlay.scss';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
export type Size = 'small' | 'medium' | 'full';
|
|
4
|
+
export interface OverlayProps {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
open: boolean;
|
|
7
|
+
size?: Size;
|
|
8
|
+
/** Slot rendered above the content (e.g. a particles canvas); nothing when omitted. */
|
|
9
|
+
particles?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
smallSizeMultiplier?: number;
|
|
12
|
+
mediumSizeMultiplier?: number;
|
|
13
|
+
fullSizeOffset?: number;
|
|
14
|
+
scrollThreshold?: number;
|
|
15
|
+
disableDrag?: boolean;
|
|
16
|
+
forceFull?: boolean;
|
|
17
|
+
scrollResetKey?: unknown;
|
|
18
|
+
/** Viewport width (px) at or below which the overlay switches to its mobile bottom-sheet mode. */
|
|
19
|
+
mobileBreakpoint?: number;
|
|
20
|
+
/** Shown when `children` is empty. */
|
|
21
|
+
noContentLabel: string;
|
|
22
|
+
onChangeSize?: (size: Size) => void;
|
|
23
|
+
onScrollStateChange?: (isScrolled: boolean) => void;
|
|
24
|
+
/** Fired (debounced) on scroll — lets the host react to user activity. */
|
|
25
|
+
onScrollActivity?: () => void;
|
|
26
|
+
}
|
|
27
|
+
declare const Overlay: ({ children, open, size, particles, className, onChangeSize, onScrollStateChange, onScrollActivity, smallSizeMultiplier, mediumSizeMultiplier, fullSizeOffset, scrollThreshold, disableDrag, forceFull, scrollResetKey, mobileBreakpoint, noContentLabel, }: OverlayProps) => import("react").JSX.Element | null;
|
|
28
|
+
export default Overlay;
|
|
29
|
+
//# sourceMappingURL=Overlay.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import './Overlay.scss';
|
|
3
|
+
import cn from 'classnames';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import debounce from '../../shared/debounce';
|
|
6
|
+
import { useDragGesture } from '../../shared/hooks/useDragGesture';
|
|
7
|
+
import { useWindowSize } from '../../shared/hooks/useWindowSize';
|
|
8
|
+
import { OverlayScrollContext } from './OverlayScrollContext';
|
|
9
|
+
const GAP = 70;
|
|
10
|
+
const TRANSITION_DURATION = '0.3s';
|
|
11
|
+
const DEFAULT_SCROLL_THRESHOLD = 10;
|
|
12
|
+
const DEFAULT_MOBILE_BREAKPOINT = 768;
|
|
13
|
+
const getActualViewportHeight = () => {
|
|
14
|
+
const isInIframe = window !== window.top;
|
|
15
|
+
if (isInIframe) {
|
|
16
|
+
return (document.documentElement.clientHeight || document.body.clientHeight || window.innerHeight);
|
|
17
|
+
}
|
|
18
|
+
return window.innerHeight;
|
|
19
|
+
};
|
|
20
|
+
const generateSizes = (height = getActualViewportHeight(), smallMultiplier, mediumMultiplier, fullSizeOffset) => ({
|
|
21
|
+
small: height / smallMultiplier,
|
|
22
|
+
medium: height / mediumMultiplier,
|
|
23
|
+
full: height - fullSizeOffset,
|
|
24
|
+
});
|
|
25
|
+
const Overlay = ({ children, open, size = 'medium', particles, className, onChangeSize, onScrollStateChange, onScrollActivity, smallSizeMultiplier = 12.6, mediumSizeMultiplier = 2.6, fullSizeOffset = 0, scrollThreshold = DEFAULT_SCROLL_THRESHOLD, disableDrag = false, forceFull = false, scrollResetKey, mobileBreakpoint = DEFAULT_MOBILE_BREAKPOINT, noContentLabel, }) => {
|
|
26
|
+
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
|
27
|
+
// Pure derivations of the window size — computed in render, not stored/synced via an effect.
|
|
28
|
+
const mobile = windowWidth <= mobileBreakpoint;
|
|
29
|
+
const sizes = useMemo(() => generateSizes(windowHeight, smallSizeMultiplier, mediumSizeMultiplier, fullSizeOffset), [windowHeight, smallSizeMultiplier, mediumSizeMultiplier, fullSizeOffset]);
|
|
30
|
+
const [dragging, setDragging] = useState(false);
|
|
31
|
+
const [panelHeight, setPanelHeight] = useState(() => sizes[size]);
|
|
32
|
+
const [overlayHeight, setOverlayHeight] = useState(() => sizes[size]);
|
|
33
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
34
|
+
const [shouldRender, setShouldRender] = useState(open);
|
|
35
|
+
const [isScrolled, setIsScrolled] = useState(false);
|
|
36
|
+
const touchStartRef = useRef(undefined);
|
|
37
|
+
const dragOffsetRef = useRef(undefined);
|
|
38
|
+
const overlayPanelRef = useRef(null);
|
|
39
|
+
const triggerRef = useRef(null);
|
|
40
|
+
const scrollRef = useRef(null);
|
|
41
|
+
// Mirror frequently-changing values into refs so the drag callbacks can stay
|
|
42
|
+
// identity-stable and the listener effect subscribes only when mobile/disableDrag change.
|
|
43
|
+
const sizesRef = useRef(sizes);
|
|
44
|
+
const panelHeightRef = useRef(panelHeight);
|
|
45
|
+
const overlayHeightRef = useRef(overlayHeight);
|
|
46
|
+
const draggingRef = useRef(dragging);
|
|
47
|
+
const sizeRef = useRef(size);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
sizesRef.current = sizes;
|
|
50
|
+
panelHeightRef.current = panelHeight;
|
|
51
|
+
overlayHeightRef.current = overlayHeight;
|
|
52
|
+
draggingRef.current = dragging;
|
|
53
|
+
sizeRef.current = size;
|
|
54
|
+
});
|
|
55
|
+
const setSize = useCallback((newSize) => {
|
|
56
|
+
const height = sizesRef.current[newSize];
|
|
57
|
+
setPanelHeight(height);
|
|
58
|
+
setOverlayHeight(height);
|
|
59
|
+
onChangeSize?.(newSize);
|
|
60
|
+
}, [onChangeSize]);
|
|
61
|
+
const positioningStyles = useMemo(() => {
|
|
62
|
+
if (mobile) {
|
|
63
|
+
if (forceFull && open) {
|
|
64
|
+
return { height: getActualViewportHeight() };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
height: open || isAnimating ? overlayHeight : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
height: open ? 'auto' : undefined,
|
|
72
|
+
};
|
|
73
|
+
}, [mobile, open, overlayHeight, isAnimating, forceFull]);
|
|
74
|
+
const setSizeByValue = useCallback((heightVal) => {
|
|
75
|
+
const sizes = sizesRef.current;
|
|
76
|
+
const sizesArray = [sizes.small, sizes.medium, sizes.full].sort();
|
|
77
|
+
if (heightVal >= sizes.full) {
|
|
78
|
+
setSize('full');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (heightVal <= sizes.small) {
|
|
82
|
+
setSize('small');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const currentHeight = panelHeightRef.current || sizes.medium;
|
|
86
|
+
const targetSize = heightVal > currentHeight
|
|
87
|
+
? (sizesArray.find((val) => val + GAP >= heightVal) ?? sizes.full)
|
|
88
|
+
: (sizesArray.reverse().find((val) => val - GAP <= heightVal) ?? sizes.small);
|
|
89
|
+
if (targetSize === sizes.full) {
|
|
90
|
+
setSize('full');
|
|
91
|
+
}
|
|
92
|
+
else if (targetSize === sizes.medium) {
|
|
93
|
+
setSize('medium');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
setSize('small');
|
|
97
|
+
}
|
|
98
|
+
}, [setSize]);
|
|
99
|
+
const handleDragStart = useCallback((clientY) => {
|
|
100
|
+
setDragging(true);
|
|
101
|
+
touchStartRef.current = clientY;
|
|
102
|
+
}, []);
|
|
103
|
+
const handleDragMove = useCallback((clientY) => {
|
|
104
|
+
const touchStart = touchStartRef.current;
|
|
105
|
+
if (touchStart === undefined)
|
|
106
|
+
return;
|
|
107
|
+
const sizes = sizesRef.current;
|
|
108
|
+
const panelHeight = panelHeightRef.current;
|
|
109
|
+
const scrollEl = scrollRef.current;
|
|
110
|
+
if ((scrollEl?.scrollTop ?? 0) > 0 && panelHeight === sizes.full)
|
|
111
|
+
return;
|
|
112
|
+
const offset = touchStart - clientY;
|
|
113
|
+
const currentHeight = panelHeight || sizes.medium;
|
|
114
|
+
let nextHeight = currentHeight + offset;
|
|
115
|
+
if (scrollEl && panelHeight === sizes.full && nextHeight < panelHeight) {
|
|
116
|
+
scrollEl.style.overflow = 'hidden';
|
|
117
|
+
}
|
|
118
|
+
nextHeight = Math.min(Math.max(nextHeight, sizes.small), sizes.full);
|
|
119
|
+
dragOffsetRef.current = offset;
|
|
120
|
+
overlayHeightRef.current = nextHeight;
|
|
121
|
+
setOverlayHeight(nextHeight);
|
|
122
|
+
}, []);
|
|
123
|
+
const handleDragEnd = useCallback(() => {
|
|
124
|
+
setDragging(false);
|
|
125
|
+
touchStartRef.current = undefined;
|
|
126
|
+
const scrollEl = scrollRef.current;
|
|
127
|
+
if (scrollEl) {
|
|
128
|
+
scrollEl.style.removeProperty('overflow');
|
|
129
|
+
}
|
|
130
|
+
const dragOffset = dragOffsetRef.current;
|
|
131
|
+
dragOffsetRef.current = undefined;
|
|
132
|
+
const finalHeight = overlayHeightRef.current;
|
|
133
|
+
if (dragOffset !== undefined && finalHeight !== undefined) {
|
|
134
|
+
setSizeByValue(finalHeight);
|
|
135
|
+
}
|
|
136
|
+
}, [setSizeByValue]);
|
|
137
|
+
const handleTriggerClick = useCallback(() => {
|
|
138
|
+
if (draggingRef.current)
|
|
139
|
+
return;
|
|
140
|
+
const currentSize = sizeRef.current;
|
|
141
|
+
const nextSize = currentSize === 'small' ? 'medium' : currentSize === 'medium' ? 'full' : 'small';
|
|
142
|
+
setSize(nextSize);
|
|
143
|
+
}, [setSize]);
|
|
144
|
+
const { touchHandlers, mouseHandlers } = useDragGesture({
|
|
145
|
+
onDragStart: handleDragStart,
|
|
146
|
+
onDragMove: handleDragMove,
|
|
147
|
+
onDragEnd: handleDragEnd,
|
|
148
|
+
});
|
|
149
|
+
const handleScroll = useCallback(() => {
|
|
150
|
+
const scrollEl = scrollRef.current;
|
|
151
|
+
if (!scrollEl)
|
|
152
|
+
return;
|
|
153
|
+
const scrollTop = scrollEl.scrollTop;
|
|
154
|
+
const shouldAddClass = scrollTop >= scrollThreshold;
|
|
155
|
+
if (isScrolled !== shouldAddClass) {
|
|
156
|
+
setIsScrolled(shouldAddClass);
|
|
157
|
+
onScrollStateChange?.(shouldAddClass);
|
|
158
|
+
}
|
|
159
|
+
}, [scrollThreshold, isScrolled, onScrollStateChange]);
|
|
160
|
+
const debouncedScrollActivity = useMemo(() => debounce(() => {
|
|
161
|
+
onScrollActivity?.();
|
|
162
|
+
}, 250), [onScrollActivity]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!mobile || disableDrag)
|
|
165
|
+
return;
|
|
166
|
+
const overlayEl = overlayPanelRef.current;
|
|
167
|
+
const triggerEl = triggerRef.current;
|
|
168
|
+
const scrollEl = scrollRef.current;
|
|
169
|
+
const preventOverscroll = () => {
|
|
170
|
+
const panelHeight = panelHeightRef.current;
|
|
171
|
+
if (panelHeight && panelHeight < sizesRef.current.full && scrollEl) {
|
|
172
|
+
scrollEl.style.overflow = 'hidden';
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if (overlayEl) {
|
|
176
|
+
overlayEl.addEventListener('touchstart', touchHandlers.onTouchStart);
|
|
177
|
+
overlayEl.addEventListener('touchend', touchHandlers.onTouchEnd);
|
|
178
|
+
window.addEventListener('touchmove', touchHandlers.onTouchMove);
|
|
179
|
+
overlayEl.addEventListener('mousedown', mouseHandlers.onMouseDown);
|
|
180
|
+
overlayEl.addEventListener('mouseup', mouseHandlers.onMouseUp);
|
|
181
|
+
window.addEventListener('mousemove', mouseHandlers.onMouseMove);
|
|
182
|
+
}
|
|
183
|
+
if (triggerEl) {
|
|
184
|
+
triggerEl.addEventListener('mouseup', handleTriggerClick);
|
|
185
|
+
}
|
|
186
|
+
if (scrollEl) {
|
|
187
|
+
scrollEl.addEventListener('touchstart', preventOverscroll);
|
|
188
|
+
}
|
|
189
|
+
return () => {
|
|
190
|
+
if (overlayEl) {
|
|
191
|
+
overlayEl.removeEventListener('touchstart', touchHandlers.onTouchStart);
|
|
192
|
+
overlayEl.removeEventListener('touchend', touchHandlers.onTouchEnd);
|
|
193
|
+
window.removeEventListener('touchmove', touchHandlers.onTouchMove);
|
|
194
|
+
overlayEl.removeEventListener('mousedown', mouseHandlers.onMouseDown);
|
|
195
|
+
overlayEl.removeEventListener('mouseup', mouseHandlers.onMouseUp);
|
|
196
|
+
window.removeEventListener('mousemove', mouseHandlers.onMouseMove);
|
|
197
|
+
}
|
|
198
|
+
if (triggerEl) {
|
|
199
|
+
triggerEl.removeEventListener('mouseup', handleTriggerClick);
|
|
200
|
+
}
|
|
201
|
+
if (scrollEl) {
|
|
202
|
+
scrollEl.removeEventListener('touchstart', preventOverscroll);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}, [mobile, disableDrag, touchHandlers, mouseHandlers, handleTriggerClick]);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (open) {
|
|
208
|
+
setShouldRender(true);
|
|
209
|
+
setIsAnimating(true);
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
setSize(size);
|
|
212
|
+
setIsAnimating(false);
|
|
213
|
+
}, 10);
|
|
214
|
+
return () => clearTimeout(timer);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
setIsAnimating(true);
|
|
218
|
+
if (mobile) {
|
|
219
|
+
setOverlayHeight(0);
|
|
220
|
+
}
|
|
221
|
+
const timer = setTimeout(() => {
|
|
222
|
+
setShouldRender(false);
|
|
223
|
+
setIsAnimating(false);
|
|
224
|
+
setIsScrolled(false);
|
|
225
|
+
}, parseFloat(TRANSITION_DURATION) * 1000);
|
|
226
|
+
return () => clearTimeout(timer);
|
|
227
|
+
}
|
|
228
|
+
}, [open, size, setSize, mobile]);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (open && !isAnimating) {
|
|
231
|
+
scrollRef.current?.scrollTo({ top: 0 });
|
|
232
|
+
setIsScrolled(false);
|
|
233
|
+
onScrollStateChange?.(false);
|
|
234
|
+
}
|
|
235
|
+
}, [open, isAnimating, onScrollStateChange, scrollResetKey]);
|
|
236
|
+
if (!shouldRender)
|
|
237
|
+
return null;
|
|
238
|
+
return (_jsx(OverlayScrollContext, { value: scrollRef.current, children: _jsxs("div", { className: cn('efp-overlay', {
|
|
239
|
+
'efp-overlay--draggable': dragging,
|
|
240
|
+
'efp-overlay--full': size === 'full',
|
|
241
|
+
'efp-overlay--mobile': mobile,
|
|
242
|
+
'efp-overlay--desktop': !mobile,
|
|
243
|
+
'efp-overlay--hidden': !mobile && !open,
|
|
244
|
+
'efp-overlay--scrolled': isScrolled,
|
|
245
|
+
'efp-overlay--forced-full': forceFull && mobile,
|
|
246
|
+
}, className), style: positioningStyles, ref: overlayPanelRef, children: [!disableDrag && _jsx("div", { className: "efp-overlay__draghandle", ref: triggerRef }), _jsxs("div", { className: "efp-overlay__scroll", ref: scrollRef, onScroll: () => {
|
|
247
|
+
handleScroll();
|
|
248
|
+
debouncedScrollActivity();
|
|
249
|
+
}, children: [particles, _jsx("div", { className: "efp-overlay__content", children: children ? children : noContentLabel })] })] }) }));
|
|
250
|
+
};
|
|
251
|
+
export default Overlay;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposes the overlay's inner scroll container to descendants so they can
|
|
3
|
+
* observe / drive it (e.g. a virtualized list syncing its scroll position).
|
|
4
|
+
* Provided by {@link Overlay}; `null` when rendered outside an overlay.
|
|
5
|
+
*/
|
|
6
|
+
export declare const OverlayScrollContext: import("react").Context<HTMLDivElement | null>;
|
|
7
|
+
//# sourceMappingURL=OverlayScrollContext.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Exposes the overlay's inner scroll container to descendants so they can
|
|
4
|
+
* observe / drive it (e.g. a virtualized list syncing its scroll position).
|
|
5
|
+
* Provided by {@link Overlay}; `null` when rendered outside an overlay.
|
|
6
|
+
*/
|
|
7
|
+
export const OverlayScrollContext = createContext(null);
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export * from './components/EntityItem';
|
|
|
18
18
|
export { default as EntityItem } from './components/EntityItem';
|
|
19
19
|
export * from './components/EventBadge';
|
|
20
20
|
export { default as EventBadge } from './components/EventBadge';
|
|
21
|
+
export * from './components/Gallery';
|
|
22
|
+
export { default as Gallery } from './components/Gallery';
|
|
21
23
|
export * from './components/HeatmapLegend';
|
|
22
24
|
export { default as HeatmapLegend } from './components/HeatmapLegend';
|
|
23
25
|
export { default as HighlightText } from './components/HighlightText';
|
|
@@ -33,6 +35,8 @@ export * from './components/Modal';
|
|
|
33
35
|
export { default as Modal } from './components/Modal';
|
|
34
36
|
export * from './components/MultiSelectGroups';
|
|
35
37
|
export { default as MultiSelectGroups } from './components/MultiSelectGroups';
|
|
38
|
+
export * from './components/Overlay';
|
|
39
|
+
export { default as Overlay } from './components/Overlay';
|
|
36
40
|
export * from './components/OverlayGrip';
|
|
37
41
|
export { default as OverlayGrip } from './components/OverlayGrip';
|
|
38
42
|
export * from './components/PermissionNotice';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export * from './components/EntityItem';
|
|
|
23
23
|
export { default as EntityItem } from './components/EntityItem';
|
|
24
24
|
export * from './components/EventBadge';
|
|
25
25
|
export { default as EventBadge } from './components/EventBadge';
|
|
26
|
+
export * from './components/Gallery';
|
|
27
|
+
export { default as Gallery } from './components/Gallery';
|
|
26
28
|
export * from './components/HeatmapLegend';
|
|
27
29
|
export { default as HeatmapLegend } from './components/HeatmapLegend';
|
|
28
30
|
export { default as HighlightText } from './components/HighlightText';
|
|
@@ -38,6 +40,8 @@ export * from './components/Modal';
|
|
|
38
40
|
export { default as Modal } from './components/Modal';
|
|
39
41
|
export * from './components/MultiSelectGroups';
|
|
40
42
|
export { default as MultiSelectGroups } from './components/MultiSelectGroups';
|
|
43
|
+
export * from './components/Overlay';
|
|
44
|
+
export { default as Overlay } from './components/Overlay';
|
|
41
45
|
export * from './components/OverlayGrip';
|
|
42
46
|
export { default as OverlayGrip } from './components/OverlayGrip';
|
|
43
47
|
export * from './components/PermissionNotice';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused element within `element`'s tree, descending into nested shadow trees.
|
|
3
|
+
* `document.activeElement` retargets to the shadow host, so it cannot be
|
|
4
|
+
* compared against elements rendered inside a shadow root.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getActiveElement(element: Element | null): Element | null;
|
|
7
|
+
//# sourceMappingURL=activeElement.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const isFocusRoot = (node) => !!node && 'activeElement' in node;
|
|
2
|
+
/**
|
|
3
|
+
* Focused element within `element`'s tree, descending into nested shadow trees.
|
|
4
|
+
* `document.activeElement` retargets to the shadow host, so it cannot be
|
|
5
|
+
* compared against elements rendered inside a shadow root.
|
|
6
|
+
*/
|
|
7
|
+
export function getActiveElement(element) {
|
|
8
|
+
const root = element?.getRootNode();
|
|
9
|
+
let active = isFocusRoot(root) ? root.activeElement : document.activeElement;
|
|
10
|
+
while (active?.shadowRoot?.activeElement) {
|
|
11
|
+
active = active.shadowRoot.activeElement;
|
|
12
|
+
}
|
|
13
|
+
return active;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface DragGestureConfig {
|
|
2
|
+
onDragStart: (clientY: number) => void;
|
|
3
|
+
onDragMove: (clientY: number) => void;
|
|
4
|
+
onDragEnd: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const useDragGesture: ({ onDragStart, onDragMove, onDragEnd }: DragGestureConfig) => {
|
|
7
|
+
touchHandlers: {
|
|
8
|
+
onTouchStart: (event: TouchEvent) => void;
|
|
9
|
+
onTouchMove: (event: TouchEvent) => void;
|
|
10
|
+
onTouchEnd: () => void;
|
|
11
|
+
};
|
|
12
|
+
mouseHandlers: {
|
|
13
|
+
onMouseDown: (event: MouseEvent) => void;
|
|
14
|
+
onMouseMove: (event: MouseEvent) => void;
|
|
15
|
+
onMouseUp: () => void;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=useDragGesture.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
export const useDragGesture = ({ onDragStart, onDragMove, onDragEnd }) => {
|
|
3
|
+
const isDraggingRef = useRef(false);
|
|
4
|
+
const handleTouchStart = useCallback((event) => {
|
|
5
|
+
isDraggingRef.current = true;
|
|
6
|
+
onDragStart(event.touches[0].clientY);
|
|
7
|
+
}, [onDragStart]);
|
|
8
|
+
const handleTouchMove = useCallback((event) => {
|
|
9
|
+
if (!isDraggingRef.current)
|
|
10
|
+
return;
|
|
11
|
+
onDragMove(event.touches[0].clientY);
|
|
12
|
+
}, [onDragMove]);
|
|
13
|
+
const handleTouchEnd = useCallback(() => {
|
|
14
|
+
if (!isDraggingRef.current)
|
|
15
|
+
return;
|
|
16
|
+
isDraggingRef.current = false;
|
|
17
|
+
onDragEnd();
|
|
18
|
+
}, [onDragEnd]);
|
|
19
|
+
const handleMouseStart = useCallback((event) => {
|
|
20
|
+
isDraggingRef.current = true;
|
|
21
|
+
onDragStart(event.clientY);
|
|
22
|
+
}, [onDragStart]);
|
|
23
|
+
const handleMouseMove = useCallback((event) => {
|
|
24
|
+
if (!isDraggingRef.current)
|
|
25
|
+
return;
|
|
26
|
+
onDragMove(event.clientY);
|
|
27
|
+
}, [onDragMove]);
|
|
28
|
+
const handleMouseEnd = useCallback(() => {
|
|
29
|
+
if (!isDraggingRef.current)
|
|
30
|
+
return;
|
|
31
|
+
isDraggingRef.current = false;
|
|
32
|
+
onDragEnd();
|
|
33
|
+
}, [onDragEnd]);
|
|
34
|
+
const touchHandlers = useMemo(() => ({
|
|
35
|
+
onTouchStart: handleTouchStart,
|
|
36
|
+
onTouchMove: handleTouchMove,
|
|
37
|
+
onTouchEnd: handleTouchEnd,
|
|
38
|
+
}), [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
39
|
+
const mouseHandlers = useMemo(() => ({
|
|
40
|
+
onMouseDown: handleMouseStart,
|
|
41
|
+
onMouseMove: handleMouseMove,
|
|
42
|
+
onMouseUp: handleMouseEnd,
|
|
43
|
+
}), [handleMouseStart, handleMouseMove, handleMouseEnd]);
|
|
44
|
+
return { touchHandlers, mouseHandlers };
|
|
45
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
export const useWindowSize = () => {
|
|
3
|
+
const [windowSize, setWindowSize] = useState(() => {
|
|
4
|
+
const isInIframe = window !== window.top;
|
|
5
|
+
if (isInIframe) {
|
|
6
|
+
return {
|
|
7
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
8
|
+
height: document.documentElement.clientHeight || window.innerHeight,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
width: window.innerWidth,
|
|
13
|
+
height: window.innerHeight,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleResize = () => {
|
|
18
|
+
const isInIframe = window !== window.top;
|
|
19
|
+
if (isInIframe) {
|
|
20
|
+
setWindowSize({
|
|
21
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
22
|
+
height: document.documentElement.clientHeight || window.innerHeight,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setWindowSize({
|
|
27
|
+
width: window.innerWidth,
|
|
28
|
+
height: window.innerHeight,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener('resize', handleResize);
|
|
33
|
+
window.addEventListener('orientationchange', handleResize);
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('resize', handleResize);
|
|
36
|
+
window.removeEventListener('orientationchange', handleResize);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
return windowSize;
|
|
40
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/ui",
|
|
3
|
-
"version": "3.23.
|
|
3
|
+
"version": "3.23.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ExpoFP SDK internal: shared pure React UI components",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"classnames": "^2.2.6",
|
|
34
34
|
"copy-to-clipboard": "^3.2.0",
|
|
35
|
+
"react-zoom-pan-pinch-sr": "^0.2.0",
|
|
35
36
|
"resize-observer": "^1.0.4",
|
|
37
|
+
"swiper": "^9.4.1",
|
|
36
38
|
"tslib": "^2.3.0"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|