point-focus
Version:
A React component for zooming and focusing on image points via click, drag, or cursor.
532 lines (517 loc) • 31.6 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import React from 'react';
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ":root{--button-icon-color:#1e1c1c;--loader-background:#19181891;--loader-color:#fff}.styles-module_c-point-focus__Glxyg{align-items:center;cursor:zoom-in;display:inline-block;display:flex;height:100%;justify-content:center;margin:0;overflow:hidden;position:relative;width:100%}.styles-module_c-point-focus__Glxyg[data-movetype=follow] .styles-module_c-point-focus__zoom-img__HP-hu{cursor:crosshair}.styles-module_c-point-focus__Glxyg[data-movetype=drag] .styles-module_c-point-focus__zoom-img__HP-hu{cursor:grab}.styles-module_c-point-focus__overlay__T8nCD{inset:0;pointer-events:none;position:absolute;z-index:1}.styles-module_c-point-focus__placeholder__dHGk6{align-items:center;background:var(--loader-background);display:flex;inset:0;justify-content:center;position:absolute;z-index:2}.styles-module_c-point-focus__img__jCS0m{-webkit-user-drag:none;aspect-ratio:4/3;display:block;height:100%;object-fit:contain;opacity:1;pointer-events:none;transition:opacity 0ms linear var(--fade-duration),visibility 0ms linear var(--fade-duration);user-select:none;visibility:visible;width:100%}.styles-module_c-point-focus__img__jCS0m[data-hidden=true]{opacity:0;visibility:hidden}.styles-module_c-point-focus__zoom-img__HP-hu{-webkit-user-drag:none;-webkit-touch-callout:none;display:block;left:0;max-width:none!important;opacity:0;pointer-events:none;position:absolute;top:0;transform:translate(0);user-select:none;visibility:hidden;width:auto!important;will-change:transform}.styles-module_c-point-focus__zoom-img__HP-hu[data-visible=true]{cursor:zoom-out;opacity:1;pointer-events:auto;touch-action:none;visibility:visible}.styles-module_c-point-focus__button__oCELz{align-items:center;appearance:none;background:transparent;border:none;cursor:pointer;display:flex;justify-content:center;opacity:0;outline:none;padding:.3rem;position:absolute;right:2%;text-decoration:none;top:2%;transform-origin:center center;visibility:hidden}.styles-module_c-point-focus__button__oCELz[data-visible=true]{opacity:1;visibility:visible}.styles-module_c-point-focus__button__oCELz svg{filter:drop-shadow(0 0 10px rgba(0,0,0,.7));height:1rem;transform-origin:center;transition:transform .2s ease-in-out;width:1rem}.styles-module_c-point-focus__button__oCELz svg path{fill:var(--button-icon-color)}.styles-module_c-point-focus__button__oCELz:hover svg{transform:scale(1.2)}.styles-module_c-point-focus__button__oCELz:active{appearance:none;background:transparent;border:none;outline:none}.styles-module_c-point-focus__button__oCELz:active svg{transform:scale(1)}.styles-module_c-point-focus__loader__AIAkB{display:inline-block;height:48px;position:relative;width:48px}.styles-module_c-point-focus__loader__AIAkB:after,.styles-module_c-point-focus__loader__AIAkB:before{animation:styles-module_animloader__7LeVS 2s linear infinite;border:2px solid var(--loader-color);border-radius:50%;box-sizing:border-box;content:\"\";height:48px;left:0;position:absolute;top:0;width:48px}.styles-module_c-point-focus__loader__AIAkB:after{animation-delay:1s}@keyframes styles-module_animloader__7LeVS{0%{opacity:1;transform:scale(0)}to{opacity:0;transform:scale(2)}}.styles-module_c-point-focus__Glxyg .styles-module_c-fp-fallback-image__sROkG{height:100%;inset:0;object-fit:cover;object-position:center center;position:absolute;width:100%;z-index:2}";
var styles = {"c-point-focus":"styles-module_c-point-focus__Glxyg","c-point-focus__zoom-img":"styles-module_c-point-focus__zoom-img__HP-hu","c-point-focus__overlay":"styles-module_c-point-focus__overlay__T8nCD","c-point-focus__placeholder":"styles-module_c-point-focus__placeholder__dHGk6","c-point-focus__img":"styles-module_c-point-focus__img__jCS0m","c-point-focus__button":"styles-module_c-point-focus__button__oCELz","c-point-focus__loader":"styles-module_c-point-focus__loader__AIAkB","animloader":"styles-module_animloader__7LeVS","c-fp-fallback-image":"styles-module_c-fp-fallback-image__sROkG"};
styleInject(css_248z);
function isTouchEvent(e) {
return 'touches' in e || ('nativeEvent' in e && 'touches' in e.nativeEvent);
}
// Defaulted values to 0 to make the functions SSR safe
function getPageCoords(e, fallbackElement) {
if ('touches' in e && e.touches && e.touches.length > 0) {
return { x: e.touches[0].pageX, y: e.touches[0].pageY };
}
else if ('pageX' in e && 'pageY' in e) {
return { x: e.pageX, y: e.pageY }; // Could further narrow with type guards
}
else if (fallbackElement) {
const rect = fallbackElement.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
return { x: 0, y: 0 };
}
const getOffsets = (pageX, pageY, left, top) => {
return {
x: pageX - left,
y: pageY - top,
};
};
const getBounds = (container) => {
return container ? container.getBoundingClientRect() : { width: 0, height: 0, left: 0, top: 0 };
};
const getRatios = (bounds, natural, zoomScale) => {
const scaledWidth = natural.width * zoomScale;
const scaledHeight = natural.height * zoomScale;
return {
x: (scaledWidth - bounds.width) / bounds.width,
y: (scaledHeight - bounds.height) / bounds.height,
};
};
function clampToBounds(left, top, bounds) {
return {
left: Math.max(Math.min(left, bounds.maxLeft), bounds.minLeft),
top: Math.max(Math.min(top, bounds.maxTop), bounds.minTop),
};
}
function calculateDragPosition(pageX, pageY, offsets) {
return {
left: pageX - offsets.x,
top: pageY - offsets.y,
};
}
const getDefaults = () => {
const defaultCoordinates = { x: 0, y: 0 };
return {
onLoadCallback: null,
bounds: { width: 0, height: 0, left: 0, top: 0 },
offsets: defaultCoordinates,
ratios: defaultCoordinates,
scaledDimensions: { width: 0, height: 0 },
dragStartCoords: defaultCoordinates,
wasDragging: false,
velocity: { vx: 0, vy: 0 },
prevDragCoords: { x: 0, y: 0, time: 0 },
lastDragCoords: { x: 0, y: 0, time: 0 },
};
};
class ZoomFallbackBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('ZoomImage runtime error:', error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? null;
}
return this.props.children;
}
}
const applyMouseMove = (pageX, pageY, zoomContextRef, setLeft, setTop) => {
const { left, top } = calculateDragPosition(pageX, pageY, zoomContextRef.current.offsets);
const bounds = { minLeft: 0, maxLeft: zoomContextRef.current.bounds.width, minTop: 0, maxTop: zoomContextRef.current.bounds.height };
const clamped = clampToBounds(left, top, bounds);
setLeft(clamped.left * -zoomContextRef.current.ratios.x);
setTop(clamped.top * -zoomContextRef.current.ratios.y);
};
const initializeFollowZoomPosition = (pageX, pageY, container, zoomContextRef, setLeft, setTop) => {
// SSR check: only run if window and container are available
if (typeof window === 'undefined' || !container) {
zoomContextRef.current.bounds = { left: 0, top: 0, width: 0, height: 0 };
zoomContextRef.current.offsets = { x: 0, y: 0 };
setLeft(0);
setTop(0);
return;
}
const bounds = getBounds(container);
zoomContextRef.current.bounds = bounds;
const offsets = getOffsets(window.pageXOffset, window.pageYOffset, -bounds.left, -bounds.top);
zoomContextRef.current.offsets = offsets;
applyMouseMove(pageX, pageY, zoomContextRef, setLeft, setTop);
};
function applyDragMove(x, y, zoomContextRef, setLeft, setTop) {
// This is setting up the animation
const now = Date.now();
zoomContextRef.current.prevDragCoords = zoomContextRef.current.lastDragCoords;
zoomContextRef.current.lastDragCoords = { x, y, time: now };
// Calculate new position using the stored offset
const { left: newLeft, top: newTop } = calculateDragPosition(x, y, zoomContextRef.current.dragStartCoords);
// Clamp to bounds
const { width, height } = zoomContextRef.current.bounds;
const maxLeft = 0;
const minLeft = width * -zoomContextRef.current.ratios.x;
const maxTop = 0;
const minTop = height * -zoomContextRef.current.ratios.y;
const clamped = clampToBounds(newLeft, newTop, { minLeft, maxLeft, minTop, maxTop });
setLeft(clamped.left);
setTop(clamped.top);
}
const CloseIcon = ({ className = 'c-zoom-close-button-icon' }) => (jsx("svg", { xmlns: 'http://www.w3.org/2000/svg', className: className, viewBox: '0 0 24 24', fill: 'none', children: jsx("path", { fillRule: 'evenodd', clipRule: 'evenodd', d: 'M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z', fill: '#000' }) }));
const FallbackImage = ({ className = 'c-fp-fallback-image' }) => (jsxs("svg", { className: className, viewBox: '0 0 2153 1548', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: [jsx("rect", { width: '100%', height: '100%', fill: 'white' }), jsx("path", { d: 'M762 504.5H1392C1425.97 504.5 1453.5 532.034 1453.5 566V982C1453.5 1015.97 1425.97 1043.5 1392 1043.5H762C728.034 1043.5 700.5 1015.97 700.5 982V566C700.5 532.034 728.034 504.5 762 504.5Z', fill: '#EBEEF0', stroke: '#8495A2', strokeWidth: '25' }), jsx("path", { d: 'M901.252 812.329C880.253 832.829 710.25 1012.23 707.251 1014.33C704.252 1016.43 728.751 1038.83 745.252 1038.83C761.752 1038.83 1381.75 1039.33 1388.25 1038.83C1394.75 1038.33 1443.75 1036.33 1445.75 981.832C1447.75 927.332 1445.75 946.332 1445.75 946.332C1445.75 946.332 1243.75 723.83 1225.75 705.829C1207.75 687.827 1194.25 681.832 1170.25 705.829C1146.25 729.826 1012.47 866.329 1012.47 866.329C1012.47 866.329 976.253 827.329 957.752 808.829C939.252 790.329 922.252 791.829 901.252 812.329Z', fill: '#CBD4DB' }), jsx("path", { d: 'M1056.25 912.329L1012.47 866.329M1012.47 866.329C1012.47 866.329 976.253 827.329 957.752 808.829C939.252 790.329 922.252 791.829 901.252 812.329C880.253 832.829 710.25 1012.23 707.251 1014.33C704.252 1016.43 728.751 1038.83 745.252 1038.83C761.752 1038.83 1381.75 1039.33 1388.25 1038.83C1394.75 1038.33 1443.75 1036.33 1445.75 981.832C1447.75 927.332 1445.75 946.332 1445.75 946.332C1445.75 946.332 1243.75 723.83 1225.75 705.829C1207.75 687.827 1194.25 681.832 1170.25 705.829C1146.25 729.826 1012.47 866.329 1012.47 866.329Z', stroke: '#8595A1', strokeWidth: '25', strokeLinecap: 'round' }), jsx("circle", { cx: '891.5', cy: '689.5', r: '60', fill: '#CBD4DA', stroke: '#8496A2', strokeWidth: '25' })] }));
const BaseImage = ({ src, sources, baseImageStyle = {}, isZoomed, fadeDuration, baseImageClassName, alt, loadingPlaceholder, errorPlaceholder, onError, disableLoadingFallbacks = false, disableErrorFallbacks = false, }) => {
const imgRef = React.useRef(null);
const [isLoading, setIsLoading] = React.useState(true);
const [hasError, setHasError] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
setHasError(false);
}, [src]);
const handleLoad = () => {
setIsLoading(false);
};
const handleError = () => {
setHasError(true);
setIsLoading(false);
onError?.();
};
React.useEffect(() => {
const img = imgRef.current;
if (img && img.complete && img.naturalWidth > 0) {
handleLoad();
}
}, [src]);
const imageStyle = React.useMemo(() => ({
'--fade-duration': `${isZoomed ? fadeDuration : 0}ms`,
...baseImageStyle,
}), [isZoomed, fadeDuration, baseImageStyle]);
const imageClass = [styles['c-point-focus__img'], baseImageClassName].filter(Boolean).join(' ');
const defaultImg = !hasError && src ? (jsx("img", { ref: imgRef, alt: alt, src: src, style: imageStyle, onLoad: handleLoad, onError: handleError, className: imageClass, "data-hidden": isZoomed, "data-testid": 'pf-base-image', crossOrigin: 'anonymous' })) : null;
const wrappedImage = sources && sources.length > 0 ? (jsxs("picture", { style: { width: '100%', height: '100%' }, children: [sources
.filter(s => s.srcSet)
.map((source, i) => (jsx("source", { ...source }, i))), defaultImg] })) : (defaultImg);
return (jsxs(Fragment, { children: [wrappedImage, !disableLoadingFallbacks && isLoading && !hasError && (jsx("div", { className: styles['c-point-focus__placeholder'], style: { zIndex: 0 }, "data-testid": 'pf-base-loading', children: loadingPlaceholder ?? jsx("span", { className: styles['c-point-focus__loader'] }) })), !disableErrorFallbacks && hasError && (jsx("div", { className: styles['c-point-focus__placeholder'], style: { zIndex: 0 }, "data-testid": 'pf-base-error', children: errorPlaceholder ?? jsx(FallbackImage, {}) }))] }));
};
const ZoomImage = React.forwardRef(({ src, fadeDuration, top, left, isZoomed, onLoad, onDragStart, onDragEnd, onClose, onFadeOut, closeButtonRef, onTouchStart, onTouchEnd, zoomImageClassName, closeButtonClassName, alt, closeButtonAriaLabel, zoomImageAriaLabel, onKeyDown, closeButtonContent, onError, loadingPlaceholder, errorPlaceholder, zoomScale, disableLoadingFallbacks = false, disableErrorFallbacks = false, }, ref) => {
const [isLoading, setIsLoading] = React.useState(true);
const [hasError, setHasError] = React.useState(false);
React.useEffect(() => {
if (!isZoomed) {
setIsLoading(true);
setHasError(false);
}
}, [isZoomed]);
const handleLoad = (e) => {
setIsLoading(false);
onLoad?.(e);
};
const handleError = () => {
setIsLoading(false);
setHasError(true);
onError?.();
};
const zoomImgClass = [styles['c-point-focus__zoom-img'], zoomImageClassName].filter(Boolean).join(' ');
return (jsxs(Fragment, { children: [jsx("img", { ref: ref, "data-testid": 'pf-zoom-image', "data-visible": isZoomed, className: zoomImgClass, style: {
transform: `translate(${left}px, ${top}px) scale(${zoomScale})`,
transformOrigin: 'top left',
transition: `opacity ${fadeDuration}ms linear, visibility ${fadeDuration}ms linear`,
}, src: src, onLoad: handleLoad, onError: handleError, onMouseDown: onDragStart, onMouseUp: onDragEnd, onTouchStart: onTouchStart, onTouchEnd: onTouchEnd, onTransitionEnd: onFadeOut, draggable: false, alt: `${alt} zoomed`, "aria-label": zoomImageAriaLabel, onKeyDown: onKeyDown, tabIndex: 0 }), !disableErrorFallbacks && hasError && isZoomed && (jsx("div", { className: styles['c-point-focus__placeholder'], "data-testid": 'pf-zoom-error', children: errorPlaceholder ?? jsx(FallbackImage, {}) })), !disableLoadingFallbacks && isLoading && !hasError && isZoomed && (jsx("div", { className: styles['c-point-focus__placeholder'], "data-testid": 'pf-zoom-loading', children: loadingPlaceholder ?? jsx("span", { className: styles['c-point-focus__loader'] }) })), onClose && (jsx("button", { type: 'button', ref: closeButtonRef, title: 'Close Zoom', "data-testid": 'pf-close-button', "aria-label": closeButtonAriaLabel ?? 'Zoom out', "data-visible": isZoomed, "data-action-type": 'close', className: closeButtonClassName || styles['c-point-focus__button'], style: {
transition: `opacity ${fadeDuration}ms linear, visibility ${fadeDuration}ms linear`,
}, onClick: () => onClose?.('click'), onKeyDown: onKeyDown, children: closeButtonContent ?? jsx(CloseIcon, {}) }))] }));
});
ZoomImage.displayName = 'ZoomImage';
function startInertia({ initialLeft, initialTop, velocity, setLeft, setTop, bounds, friction = 0.95, minVelocity = 10, onEnd, contextRef, }) {
let left = initialLeft;
let top = initialTop;
let vx = velocity.vx;
let vy = velocity.vy;
function step() {
vx *= friction;
vy *= friction;
left += vx * (1 / 60);
top += vy * (1 / 60);
const clamped = clampToBounds(left, top, bounds);
setLeft(clamped.left);
setTop(clamped.top);
if (Math.abs(vx) > minVelocity || Math.abs(vy) > minVelocity) {
contextRef.current.rafId = requestAnimationFrame(step); // 👈 Store rafId
}
else {
contextRef.current.rafId = null;
setLeft(clamped.left);
setTop(clamped.top);
onEnd?.();
}
}
contextRef.current.rafId = requestAnimationFrame(step);
}
const ImageMagnifier = ({ src, sources, width, height, zoomSrc, zoomScale = 1, zoomPreload, fadeDuration = 150, moveType = 'follow', zoomType = 'click', baseImageStyle, hideCloseButton, containerClassName, baseImageClassName, zoomImageClassName, closeButtonClassName, onMouseEnter, onZoomedMouseMove, onMouseLeave, onClickImage, onZoom, onClose, afterZoomImgLoaded, afterZoomOut, onBaseImageError, onZoomImageError, onDragStart, onDragEnd, alt, containerAriaLabel, closeButtonAriaLabel, zoomImageAriaLabel, tabIndex = 0, closeButtonContent, overlay, disableDrag, disableInertia, loadingPlaceholder, errorPlaceholder, clickToZoomOut = false, externalZoomState, setExternalZoomState, disableMobile, disableLoadingFallbacks, disableErrorFallbacks, }) => {
const containerRef = React.useRef(null);
const closeButtonRef = React.useRef(null);
const zoomedImgRef = React.useRef(null);
const zoomContextRef = React.useRef(getDefaults());
const isControlled = externalZoomState !== undefined;
const [internalZoom, setInternalZoom] = React.useState(false);
const isZoomed = isControlled ? externalZoomState : internalZoom;
const [isActive, setIsActive] = React.useState(zoomPreload);
const [isDragging, setIsDragging] = React.useState(false);
const [isFading, setIsFading] = React.useState(false);
const [left, setLeft] = React.useState(0);
const [top, setTop] = React.useState(0);
const safeZoomScale = Math.max(1, Math.min(zoomScale, 5));
const updateZoomState = (val) => {
if (isControlled) {
setExternalZoomState?.(val);
}
else {
setInternalZoom(val);
}
};
const zoomIn = (pageX, pageY, method) => {
updateZoomState(true);
initializeFollowZoomPosition(pageX, pageY, containerRef.current, zoomContextRef, setLeft, setTop);
onZoom?.({ at: { x: pageX, y: pageY }, method: method });
setTimeout(() => {
closeButtonRef.current?.focus();
}, 0);
};
const zoomOut = () => {
if (!zoomPreload) {
setIsFading(true);
}
zoomContextRef.current.onLoadCallback = null;
zoomContextRef.current.wasDragging = false;
zoomContextRef.current.dragStartCoords = { x: 0, y: 0 };
zoomContextRef.current.velocity = null;
zoomContextRef.current.prevDragCoords = null;
zoomContextRef.current.lastDragCoords = null;
setLeft(0);
setTop(0);
updateZoomState(false);
};
const handleDragStart = React.useCallback((e) => {
if (!isZoomed || disableDrag || (disableMobile && isTouchEvent(e)))
return;
if (zoomContextRef.current.rafId != null) {
cancelAnimationFrame(zoomContextRef.current.rafId);
zoomContextRef.current.rafId = null;
}
const { x, y } = getPageCoords(e);
setIsDragging(true);
zoomContextRef.current.dragStartCoords = getOffsets(x, y, left, top);
onDragStart?.({
start: { x, y },
source: isTouchEvent(e) ? 'touch' : 'mouse',
});
}, [isZoomed, disableDrag, disableMobile, left, top, onDragStart]);
const handleDragMove = React.useCallback((e) => {
if (!isDragging)
return;
const { x, y } = getPageCoords(e);
applyDragMove(x, y, zoomContextRef, setLeft, setTop);
if ('touches' in e)
e.preventDefault?.();
}, [isDragging, setLeft, setTop]);
const handleDragEnd = React.useCallback(() => {
setIsDragging(false);
zoomContextRef.current.dragStartCoords = { x: 0, y: 0 };
// Animation on end
// Calculate velocity
const last = zoomContextRef.current.lastDragCoords;
const prev = zoomContextRef.current.prevDragCoords;
let vx = 0, vy = 0;
if (last && prev) {
const dt = (last.time - prev.time) / 1000;
if (dt > 0) {
vx = (last.x - prev.x) / dt;
vy = (last.y - prev.y) / dt;
}
}
zoomContextRef.current.velocity = { vx, vy };
// Calculate bounds
const { width, height } = zoomContextRef.current.bounds;
const maxLeft = 0;
const minLeft = width * -zoomContextRef.current.ratios.x;
const maxTop = 0;
const minTop = height * -zoomContextRef.current.ratios.y;
if (!disableInertia) {
startInertia({
initialLeft: left,
initialTop: top,
velocity: { vx, vy },
setLeft,
setTop,
bounds: { minLeft, maxLeft, minTop, maxTop },
friction: 0.95,
minVelocity: 10,
onEnd: () => {
// Optionally, snap to bounds or do something else
},
contextRef: zoomContextRef,
});
}
onDragEnd?.({
velocity: { vx, vy },
final: { x: left, y: top },
});
}, [disableInertia, left, top, setLeft, setTop, onDragEnd]);
const applyImageLoad = (el) => {
const naturalDimensions = {
width: el.naturalWidth,
height: el.naturalHeight,
};
const bounds = getBounds(containerRef.current);
zoomedImgRef.current = el;
zoomContextRef.current.scaledDimensions = {
width: naturalDimensions.width * safeZoomScale,
height: naturalDimensions.height * safeZoomScale,
};
zoomContextRef.current.bounds = bounds;
zoomContextRef.current.ratios = getRatios(bounds, naturalDimensions, safeZoomScale);
if (zoomContextRef.current.onLoadCallback) {
zoomContextRef.current.onLoadCallback();
zoomContextRef.current.onLoadCallback = null;
}
};
const handleLoad = React.useCallback((e) => {
applyImageLoad(e.currentTarget);
afterZoomImgLoaded?.();
}, [applyImageLoad, afterZoomImgLoaded]);
const handleClose = React.useCallback((method) => {
onClose?.({ triggeredBy: method });
zoomOut();
}, [onClose, zoomOut]);
const handleClick = React.useCallback((e, method) => {
if (disableMobile && isTouchEvent(e))
return;
const { x, y } = getPageCoords(e);
onClickImage?.({ x, y, source: isTouchEvent(e) ? 'touch' : 'mouse' });
if (zoomContextRef.current.wasDragging) {
zoomContextRef.current.wasDragging = false;
return;
}
if (isZoomed) {
if (clickToZoomOut) {
handleClose(method);
}
return;
} // is important to keep it to avoid issues when dragging the image with the animation
if (zoomedImgRef.current) {
applyImageLoad(zoomedImgRef.current);
zoomIn(x, y, method);
}
else {
zoomContextRef.current.onLoadCallback = () => zoomIn(x, y, method);
}
}, [disableMobile, isTouchEvent, getPageCoords, onClickImage, isZoomed, clickToZoomOut, handleClose, applyImageLoad, zoomIn]);
const handleKeyDown = React.useCallback((e) => {
if ((e.key === 'Enter' || e.key === ' ') && !isZoomed) {
e.preventDefault();
const { x, y } = getPageCoords(e, containerRef.current);
zoomIn(x, y, 'keyboard');
}
if (e.key === 'Escape' && isZoomed) {
e.preventDefault();
handleClose('keyboard');
}
}, [isZoomed, getPageCoords, zoomIn, handleClose]);
const handleZoomLayerKeyDown = React.useCallback((e) => {
if (e.key === 'Escape' && isZoomed) {
e.preventDefault();
handleClose('keyboard');
}
}, [isZoomed, handleClose]);
const throttledZoomedMouseMove = React.useCallback((x, y, bounds) => {
zoomContextRef.current.lastMoveEvent = { x, y, bounds };
if (zoomContextRef.current.rafId == null) {
zoomContextRef.current.rafId = window.requestAnimationFrame(() => {
const evt = zoomContextRef.current.lastMoveEvent;
if (evt && onZoomedMouseMove) {
onZoomedMouseMove(evt);
}
zoomContextRef.current.rafId = null;
});
}
}, [onZoomedMouseMove]);
const handleMouseEnter = React.useCallback((e) => {
setIsActive(true);
setIsFading(false);
onMouseEnter?.({ source: isTouchEvent(e) ? 'touch' : 'mouse' });
if (zoomType === 'hover' && !isZoomed && !(disableMobile && isTouchEvent(e))) {
handleClick(e, 'hover');
}
}, [setIsActive, setIsFading, onMouseEnter, zoomType, isZoomed, disableMobile, handleClick]);
const handleMouseMove = React.useCallback((e) => {
if (moveType === 'follow' && isZoomed) {
const { x, y } = getPageCoords(e);
const bounds = zoomContextRef.current.bounds;
if (!bounds || typeof bounds.left !== 'number')
return;
throttledZoomedMouseMove(x, y, zoomContextRef.current.bounds);
applyMouseMove(x, y, zoomContextRef, setLeft, setTop);
}
}, [moveType, isZoomed, throttledZoomedMouseMove, setLeft, setTop]);
const handleMouseLeave = React.useCallback((e) => {
onMouseLeave?.({ source: isTouchEvent(e) ? 'touch' : 'mouse' });
handleClose('hover');
}, [onMouseLeave, handleClose]);
const handleFadeOut = React.useCallback((e, noTransition) => {
if (noTransition || (e && 'propertyName' in e && e.propertyName === 'opacity' && containerRef?.current?.contains(e.target))) {
if (!zoomPreload) {
zoomedImgRef.current = null;
zoomContextRef.current = getDefaults();
setIsActive(false);
}
setIsFading(false);
afterZoomOut?.();
}
}, [zoomPreload, setIsActive, setIsFading, afterZoomOut]);
React.useEffect(() => {
zoomContextRef.current = getDefaults();
return () => {
if (zoomContextRef.current.rafId != null) {
cancelAnimationFrame(zoomContextRef.current.rafId);
zoomContextRef.current.rafId = null;
}
};
}, []);
React.useEffect(() => {
if (isDragging && !disableDrag) {
window.addEventListener('mousemove', handleDragMove);
window.addEventListener('mouseup', handleDragEnd);
window.addEventListener('touchmove', handleDragMove, { passive: false });
window.addEventListener('touchend', handleDragEnd);
return () => {
window.removeEventListener('mousemove', handleDragMove);
window.removeEventListener('mouseup', handleDragEnd);
window.removeEventListener('touchmove', handleDragMove);
window.removeEventListener('touchend', handleDragEnd);
};
}
}, [isDragging]);
const zoomImageProps = {
src: zoomSrc || src,
fadeDuration: fadeDuration,
top,
left,
isZoomed,
onLoad: handleLoad,
onError: onZoomImageError,
onDragStart: moveType === 'drag' && !disableDrag ? handleDragStart : undefined,
onDragEnd: moveType === 'drag' && !disableDrag ? handleDragEnd : undefined,
onTouchStart: handleDragStart,
onTouchEnd: handleDragEnd,
onClose: !hideCloseButton ? handleClose : undefined,
onFadeOut: isFading ? handleFadeOut : undefined,
closeButtonRef: closeButtonRef,
zoomImageClassName: zoomImageClassName,
closeButtonClassName: closeButtonClassName,
alt: alt,
closeButtonAriaLabel: closeButtonAriaLabel,
zoomImageAriaLabel: zoomImageAriaLabel,
onKeyDown: handleZoomLayerKeyDown,
closeButtonContent: closeButtonContent,
loadingPlaceholder: loadingPlaceholder,
errorPlaceholder: errorPlaceholder,
zoomScale: safeZoomScale,
disableLoadingFallbacks: disableLoadingFallbacks,
disableErrorFallbacks: disableErrorFallbacks,
};
const containerClass = [styles['c-point-focus'], containerClassName].filter(Boolean).join(' ');
return (jsxs("figure", { role: 'group', ref: containerRef, tabIndex: tabIndex, "data-movetype": moveType, "data-testid": 'pf-image-magnifier-container', "aria-label": containerAriaLabel ?? 'Zoomable image', className: containerClass, style: { width: width, height: height }, onClick: e => handleClick(e, 'hover'), onTouchStart: e => {
if (disableMobile) {
e.preventDefault();
}
}, onKeyDown: handleKeyDown, onMouseEnter: handleMouseEnter, onMouseMove: isZoomed ? handleMouseMove : undefined, onMouseLeave: handleMouseLeave, children: [jsx(BaseImage, { src: src, alt: alt, sources: sources, width: width, height: height, baseImageStyle: baseImageStyle, baseImageClassName: baseImageClassName, fadeDuration: fadeDuration, isZoomed: isZoomed, onError: onBaseImageError, loadingPlaceholder: loadingPlaceholder, errorPlaceholder: errorPlaceholder, disableLoadingFallbacks: disableLoadingFallbacks, disableErrorFallbacks: disableErrorFallbacks }), isActive && (jsxs(Fragment, { children: [overlay && (jsx("div", { className: styles['c-point-focus__overlay'], "data-testid": 'pf-overlay', children: overlay })), jsx(ZoomFallbackBoundary, { fallback: errorPlaceholder ?? jsx(FallbackImage, {}), children: jsx(ZoomImage, { ref: zoomedImgRef, ...zoomImageProps }, isZoomed && clickToZoomOut ? 'zoomed' : 'unzoomed') })] }))] }));
};
export { ImageMagnifier as default };
//# sourceMappingURL=index.js.map