From 96a906a6b19b140b0165b4892c2fdf678b3e6543 Mon Sep 17 00:00:00 2001 From: Quentin Savoye Date: Thu, 4 Dec 2025 16:05:32 +0100 Subject: [PATCH] feat: enhance TransformWrapper to support parent context for nested zooming functionality --- .../transform-wrapper/transform-wrapper.tsx | 21 +++++++-- src/core/handlers/handlers.logic.ts | 46 +++++++++++++------ src/core/instance.core.ts | 20 +++++++- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/components/transform-wrapper/transform-wrapper.tsx b/src/components/transform-wrapper/transform-wrapper.tsx index bdada8af..393de746 100644 --- a/src/components/transform-wrapper/transform-wrapper.tsx +++ b/src/components/transform-wrapper/transform-wrapper.tsx @@ -1,4 +1,9 @@ -import React, { useEffect, useImperativeHandle, useRef } from "react"; +import React, { + useContext, + useEffect, + useImperativeHandle, + useRef, +} from "react"; import { ZoomPanPinch } from "../../core/instance.core"; import { @@ -9,6 +14,9 @@ import { getControls } from "../../utils"; export const Context = React.createContext(null as any); +// Context pour détecter un TransformWrapper parent +const ParentContext = React.createContext(null); + const getContent = ( children: ReactZoomPanPinchProps["children"], ctx: ReactZoomPanPinchContentRef, @@ -24,7 +32,10 @@ export const TransformWrapper = React.forwardRef( props: Omit, ref: React.Ref, ) => { - const instance = useRef(new ZoomPanPinch(props)).current; + // Vérifier s'il y a un TransformWrapper parent via le Context + const parentContext = useContext(ParentContext); + + const instance = useRef(new ZoomPanPinch(props, parentContext)).current; const content = getContent(props.children, getControls(instance)); @@ -34,6 +45,10 @@ export const TransformWrapper = React.forwardRef( instance.update(props); }, [instance, props]); - return {content}; + return ( + + {content} + + ); }, ); diff --git a/src/core/handlers/handlers.logic.ts b/src/core/handlers/handlers.logic.ts index 84c8317f..47a7720f 100644 --- a/src/core/handlers/handlers.logic.ts +++ b/src/core/handlers/handlers.logic.ts @@ -1,4 +1,4 @@ -import { ReactZoomPanPinchContext } from "../../models"; +import { ReactZoomPanPinchContext, StateType } from "../../models"; import { calculateZoomToNode, handleZoomToViewCenter, @@ -8,6 +8,19 @@ import { animations } from "../animations/animations.constants"; import { animate, handleCancelAnimation } from "../animations/animations.utils"; import { getCenterPosition } from "../../utils"; +function applyParentScale( + targetState: StateType, + contextInstance: ReactZoomPanPinchContext, +): StateType { + const result = targetState; + if (contextInstance.parentInstance !== null) { + result.positionX /= contextInstance.parentInstance.transformState.scale; + result.positionY /= contextInstance.parentInstance.transformState.scale; + return applyParentScale(targetState, contextInstance.parentInstance); + } + return result; +} + export const zoomIn = (contextInstance: ReactZoomPanPinchContext) => ( @@ -55,11 +68,14 @@ export const setTransform = if (disabled || !wrapperComponent || !contentComponent) return; - const targetState = { - positionX: Number.isNaN(newPositionX) ? positionX : newPositionX, - positionY: Number.isNaN(newPositionY) ? positionY : newPositionY, - scale: Number.isNaN(newScale) ? scale : newScale, - }; + const targetState = applyParentScale( + { + positionX: Number.isNaN(newPositionX) ? positionX : newPositionX, + positionY: Number.isNaN(newPositionY) ? positionY : newPositionY, + scale: Number.isNaN(newScale) ? scale : newScale, + }, + contextInstance, + ); animate(contextInstance, targetState, animationTime, animationType); }; @@ -83,10 +99,13 @@ export const centerView = const { transformState, wrapperComponent, contentComponent } = contextInstance; if (wrapperComponent && contentComponent) { - const targetState = getCenterPosition( - scale || transformState.scale, - wrapperComponent, - contentComponent, + const targetState = applyParentScale( + getCenterPosition( + scale || transformState.scale, + wrapperComponent, + contentComponent, + ), + contextInstance, ); animate(contextInstance, targetState, animationTime, animationType); @@ -111,12 +130,9 @@ export const zoomToElement = typeof node === "string" ? document.getElementById(node) : node; if (wrapperComponent && target && wrapperComponent.contains(target)) { - const targetState = calculateZoomToNode( + const targetState = applyParentScale( + calculateZoomToNode(contextInstance, target, scale, offsetX, offsetY), contextInstance, - target, - scale, - offsetX, - offsetY, ); animate(contextInstance, targetState, animationTime, animationType); } diff --git a/src/core/instance.core.ts b/src/core/instance.core.ts index c7139651..b2508ec3 100644 --- a/src/core/instance.core.ts +++ b/src/core/instance.core.ts @@ -7,6 +7,7 @@ import { ReactZoomPanPinchProps, ReactZoomPanPinchState, ReactZoomPanPinchRef, + StateType, } from "../models"; import { getContext, @@ -101,10 +102,16 @@ export class ZoomPanPinch { // key press public pressedKeys: { [key: string]: boolean } = {}; - constructor(props: ReactZoomPanPinchProps) { + public parentInstance: ZoomPanPinch | null; + + constructor( + props: ReactZoomPanPinchProps, + parentInstance: ZoomPanPinch | null, + ) { this.props = props; this.setup = createSetup(this.props); this.transformState = createState(this.props); + this.parentInstance = parentInstance; } mount = () => { @@ -587,4 +594,15 @@ export class ZoomPanPinch { const ctx = getContext(this); handleCallback(ctx, undefined, this.props.onInit); }; + + getCenterPosition = (): StateType => { + if (this.wrapperComponent && this.contentComponent) { + return getCenterPosition( + this.transformState.scale, + this.wrapperComponent, + this.contentComponent, + ); + } + return { scale: 1, positionX: 0, positionY: 0 }; + }; }