diff --git a/__tests__/examples/example.tsx b/__tests__/examples/example.tsx index c8c455e0..bd734e92 100644 --- a/__tests__/examples/example.tsx +++ b/__tests__/examples/example.tsx @@ -38,6 +38,10 @@ export const Example = (options: { maxWidth: "100%", maxHeight: "calc(100vh - 50px)", }} + contentStyle={{ + width: "100%", + height: "100%", + }} >

Title

diff --git a/__tests__/features/pan-touch/pan-touch.base.spec.tsx b/__tests__/features/pan-touch/pan-touch.base.spec.tsx index 4cfe849d..0ef44b3a 100644 --- a/__tests__/features/pan-touch/pan-touch.base.spec.tsx +++ b/__tests__/features/pan-touch/pan-touch.base.spec.tsx @@ -9,13 +9,11 @@ describe("Pan Touch [Base]", () => { expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); pinch({ value: 1.5 }); await waitFor(() => { - expect(content.style.transform).toBe( - "translate(0px, 0px) scale(1.5012468827930179)", - ); + expect(content.style.transform).toBe("translate(0px, 0px) scale(1.5)"); }); touchPan({ x: 100, y: 100 }); expect(content.style.transform).toBe( - "translate(100px, 100px) scale(1.5012468827930179)", + "translate(100px, 100px) scale(1.5)", ); }); }); diff --git a/__tests__/features/pan/pan.base.spec.tsx b/__tests__/features/pan/pan.base.spec.tsx index a4e469f1..256631e2 100644 --- a/__tests__/features/pan/pan.base.spec.tsx +++ b/__tests__/features/pan/pan.base.spec.tsx @@ -3,17 +3,35 @@ import { waitFor } from "@testing-library/react"; import { renderApp } from "../../utils/render-app"; describe("Pan [Base]", () => { - describe("When panning in with controls button", () => { + describe("When panning to coords", () => { + it("should not change translate with disabled padding", async () => { + const { content, pan } = renderApp({ + disablePadding: true, + }); + expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); + pan({ x: 100, y: 100 }); + expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); + }); + it("should return to position with padding enabled", async () => { + const { content, pan } = renderApp({ + disablePadding: false, + }); + + expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); + pan({ x: 100, y: 100 }); + expect(content.style.transform).toBe("translate(100px, 100px) scale(1)"); + await waitFor(() => { + expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); + }); + }); it("should change translate css", async () => { const { content, pan, zoom } = renderApp(); expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); zoom({ value: 1.5 }); - expect(content.style.transform).toBe( - "translate(0px, 0px) scale(1.5009999999999448)", - ); + expect(content.style.transform).toBe("translate(0px, 0px) scale(1.5)"); pan({ x: 100, y: 100 }); expect(content.style.transform).toBe( - "translate(100px, 100px) scale(1.5009999999999448)", + "translate(100px, 100px) scale(1.5)", ); }); }); diff --git a/__tests__/features/pinch/pinch.base.spec.tsx b/__tests__/features/pinch/pinch.base.spec.tsx index 2fee9de1..ec77817a 100644 --- a/__tests__/features/pinch/pinch.base.spec.tsx +++ b/__tests__/features/pinch/pinch.base.spec.tsx @@ -4,13 +4,12 @@ import { renderApp } from "../../utils/render-app"; describe("Pinch [Base]", () => { describe("When zooming in with controls button", () => { it("should increase css scale with animated zoom", async () => { - const { content, pinch } = renderApp(); + const { ref, content, pinch } = renderApp(); expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); pinch({ value: 1.5 }); await waitFor(() => { - expect(content.style.transform).toBe( - "translate(0px, 0px) scale(1.5012468827930179)", - ); + expect(content.style.transform).toBe("translate(0px, 0px) scale(1.5)"); + expect(ref.current?.instance.transformState.scale).toBe(1.5); }); }); }); diff --git a/__tests__/features/zoom/zoom.base.spec.tsx b/__tests__/features/zoom/zoom.base.spec.tsx index ebee9f50..92978489 100644 --- a/__tests__/features/zoom/zoom.base.spec.tsx +++ b/__tests__/features/zoom/zoom.base.spec.tsx @@ -1,14 +1,16 @@ +import { waitFor } from "@testing-library/dom"; import { renderApp } from "../../utils/render-app"; describe("Zoom [Base]", () => { describe("When zooming in with controls button", () => { it("should increase css scale with animated zoom", async () => { - const { content, zoom } = renderApp(); + const { ref, content, zoom } = renderApp(); expect(content.style.transform).toBe("translate(0px, 0px) scale(1)"); zoom({ value: 1.5 }); - expect(content.style.transform).toBe( - "translate(0px, 0px) scale(1.5009999999999448)", - ); + await waitFor(() => { + expect(content.style.transform).toBe("translate(0px, 0px) scale(1.5)"); + expect(ref.current?.instance.transformState.scale).toBe(1.5); + }); }); }); }); diff --git a/__tests__/utils/render-app.tsx b/__tests__/utils/render-app.tsx index 834eade5..0e971146 100644 --- a/__tests__/utils/render-app.tsx +++ b/__tests__/utils/render-app.tsx @@ -89,18 +89,26 @@ export const renderApp = ( fireEvent.mouseMove(content, { clientX: center[0], clientY: center[1] }); } + const step = 1; + const isZoomIn = ref.current.instance.transformState.scale < value; while (true) { if ( - isZoomIn - ? ref.current.instance.transformState.scale <= value - : ref.current.instance.transformState.scale >= value + (isZoomIn + ? ref.current.instance.transformState.scale < value + : ref.current.instance.transformState.scale > value) && + ref.current.instance.transformState.scale !== value ) { + const isNearScale = + Math.abs(ref.current.instance.transformState.scale - value) < 0.01; + + const newStep = isNearScale ? 0.35 : step; + fireEvent( content, new WheelEvent("wheel", { bubbles: true, - deltaY: isZoomIn ? -1 : 1, + deltaY: isZoomIn ? -newStep : newStep, }), ); } else { @@ -127,12 +135,19 @@ export const renderApp = ( while (true) { if ( - isZoomIn - ? ref.current.instance.transformState.scale <= value - : ref.current.instance.transformState.scale >= value + (isZoomIn + ? ref.current.instance.transformState.scale < value + : ref.current.instance.transformState.scale > value) && + ref.current.instance.transformState.scale !== value ) { - pinchValue[0] = pinchValue[0] + stepX; - pinchValue[1] = pinchValue[1] + stepY; + const isNearScale = + Math.abs(ref.current.instance.transformState.scale - value) < 0.5; + + const newStepX = isNearScale ? stepX / 10 : stepX; + const newStepY = isNearScale ? stepY / 10 : stepY; + + pinchValue[0] = pinchValue[0] + newStepX; + pinchValue[1] = pinchValue[1] + newStepY; touches = getPinchTouches( content, center, @@ -155,6 +170,7 @@ export const renderApp = ( fireEvent.mouseDown(content); fireEvent.mouseMove(content, { clientX: x, clientY: y }); fireEvent.mouseUp(content); + fireEvent.blur(content); }; const touchPan: RenderApp["touchPan"] = ({ x, y }) => { diff --git a/jest.config.ts b/jest.config.ts index 53f86989..06b79afb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -36,6 +36,6 @@ const config: Config.InitialOptions = { "jest-watch-typeahead/filename", "jest-watch-typeahead/testname", ], - setupFilesAfterEnv: ["jest-extended/all"], + setupFilesAfterEnv: ["jest-extended/all", "./jest.setup.ts"], }; export default config; diff --git a/jest.setup.ts b/jest.setup.ts index a8e02001..a846a6cf 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -16,3 +16,37 @@ jestPreviewConfigure({ autoPreview: true, // publicFolder: "static", // No need to configure if `publicFolder` is `public` }); + +const getWidth = (element: HTMLElement) => { + const isPercentageWidth = element.style.width.includes("%"); + const isPercentageHeight = element.style.height.includes("%"); + + let width = 0; + let height = 0; + + const top = parseFloat(element.style.marginTop) || 0; + const left = parseFloat(element.style.marginLeft) || 0; + + if (isPercentageWidth || isPercentageHeight) { + const parent = getWidth(element.parentNode as HTMLElement); + width = (parseFloat(element.style.width) * parent.width) / 100; + height = (parseFloat(element.style.height) * parent.height) / 100; + } else { + width = parseFloat(element.style.width); + height = parseFloat(element.style.height); + } + + return { + width, + height, + top, + left, + }; +}; + +// @ts-ignore +window.HTMLElement.prototype.getBoundingClientRect = function () { + const size = getWidth(this); + + return size; +}; diff --git a/src/constants/state.constants.ts b/src/constants/state.constants.ts index 647cba49..a91614a3 100644 --- a/src/constants/state.constants.ts +++ b/src/constants/state.constants.ts @@ -22,7 +22,7 @@ export const initialSetup: LibrarySetup = { smooth: true, detached: false, wheel: { - step: 0.03, + step: 0.015, disabled: false, wheelDisabled: false, touchPadDisabled: false, @@ -71,7 +71,8 @@ export const initialSetup: LibrarySetup = { velocityAnimation: { disabled: false, sensitivity: 1, - animationTime: 400, + maxStrength: 2.5, + animationTime: 300, animationType: "easeOut", equalToMove: true, }, diff --git a/src/core/instance.core.ts b/src/core/instance.core.ts index c8773481..50e9e3ba 100644 --- a/src/core/instance.core.ts +++ b/src/core/instance.core.ts @@ -487,10 +487,10 @@ export class ZoomPanPinch { ) { if (scale !== this.transformState.scale) { this.transformState.previousScale = this.transformState.scale; - this.transformState.scale = scale; + this.transformState.scale = parseFloat(scale.toFixed(2)); } - this.transformState.positionX = positionX; - this.transformState.positionY = positionY; + this.transformState.positionX = parseFloat(positionX.toFixed(2)); + this.transformState.positionY = parseFloat(positionY.toFixed(2)); this.applyTransformation(); const ctx = getContext(this); diff --git a/src/core/pan/velocity.utils.ts b/src/core/pan/velocity.utils.ts index 57d5422e..3409c827 100644 --- a/src/core/pan/velocity.utils.ts +++ b/src/core/pan/velocity.utils.ts @@ -37,10 +37,12 @@ export function getVelocityMoveTime( velocity: number, ): number { const { velocityAnimation } = contextInstance.setup; - const { equalToMove, animationTime, sensitivity } = velocityAnimation; + const { equalToMove, animationTime, sensitivity, maxStrength } = + velocityAnimation; if (equalToMove) { - return animationTime * velocity * sensitivity; + const velocityValue = Math.min(velocity, maxStrength); + return animationTime * velocityValue * sensitivity; } return animationTime; } diff --git a/src/models/context.model.ts b/src/models/context.model.ts index 15625f70..46634217 100644 --- a/src/models/context.model.ts +++ b/src/models/context.model.ts @@ -51,7 +51,7 @@ export type ReactZoomPanPinchProps = { | React.ReactNode | ((ref: ReactZoomPanPinchContentRef) => React.ReactNode); ref?: React.Ref; - detached: boolean; + detached?: boolean; initialScale?: number; initialPositionX?: number; initialPositionY?: number; @@ -117,6 +117,7 @@ export type ReactZoomPanPinchProps = { }; velocityAnimation?: { disabled?: boolean; + maxStrength?: number; sensitivity?: number; animationTime?: number; animationType?: keyof typeof animations; diff --git a/src/stories/docs/props.tsx b/src/stories/docs/props.tsx index 71216d3a..78a6d3b5 100644 --- a/src/stories/docs/props.tsx +++ b/src/stories/docs/props.tsx @@ -165,6 +165,18 @@ export const wrapperPropsTable: ComponentProps = { description: "We can provide custom transform function to provide different way of handling our transform logic. If we need performance we can import getMatrixTransformStyles functions and replace default one. WARNING: default transform prevents svg blur on the safari.", }, + smooth: { + type: ["boolean"], + defaultValue: String(initialSetup.smooth), + description: + "Enable smooth scrolling by multiplying the scroll delta with the smooth step factor.", + }, + detached: { + type: ["boolean"], + defaultValue: String(initialSetup.smooth), + description: + "Allows to prevent CSS being applied to content element. It allows to add the functionality to canvas with some custom transforming logic.", + }, wheel: { wheel: { type: [""], @@ -176,12 +188,6 @@ export const wrapperPropsTable: ComponentProps = { defaultValue: String(initialSetup.wheel.step), description: "The sensitivity of zooming with the wheel/touchpad.", }, - smooth: { - type: ["boolean"], - defaultValue: String(initialSetup.wheel.smooth), - description: - "Enable smooth scrolling by multiplying the scroll delta with the smooth step factor.", - }, disabled: { type: ["boolean"], defaultValue: String(initialSetup.wheel.disabled), @@ -418,6 +424,12 @@ export const wrapperPropsTable: ComponentProps = { defaultValue: String(initialSetup.velocityAnimation.disabled), description: "Disable the double click feature.", }, + maxStrength: { + type: ["number"], + defaultValue: String(initialSetup.velocityAnimation.maxStrength), + description: + "The maximum strength of the velocity animation. The higher the value, the faster the animation will be allowed.", + }, sensitivity: { type: ["number"], defaultValue: String(initialSetup.velocityAnimation.animationTime), diff --git a/src/stories/examples/content-rerendering/example.tsx b/src/stories/examples/content-rerendering/example.tsx index f010e1ae..5511c272 100644 --- a/src/stories/examples/content-rerendering/example.tsx +++ b/src/stories/examples/content-rerendering/example.tsx @@ -40,7 +40,7 @@ export const Example: React.FC = (args: any) => { background: "#444", color: "white", padding: "50px", - minHeight: "300px", + minHeight: "100%", width: "100%", }} > diff --git a/src/stories/examples/stress/example.tsx b/src/stories/examples/stress/example.tsx index 3ed15d7c..3ec3963c 100644 --- a/src/stories/examples/stress/example.tsx +++ b/src/stories/examples/stress/example.tsx @@ -11,7 +11,7 @@ export const Example: React.FC = (args: any) => { return ( - +
{Array.from(Array(1000).keys()).map((key) => ( diff --git a/src/stories/types/args.types.ts b/src/stories/types/args.types.ts index a0588533..821b443c 100644 --- a/src/stories/types/args.types.ts +++ b/src/stories/types/args.types.ts @@ -16,15 +16,6 @@ export const argsTypes = { defaultValue: { summary: "0" }, }, }, - "wheel.smooth": { - defaultValue: initialSetup.wheel.smooth, - control: { - type: "boolean", - }, - table: { - defaultValue: { summary: "true" }, - }, - }, "wheel.disabled": { defaultValue: initialSetup.wheel.disabled, control: { type: "boolean" }, diff --git a/src/stories/utils/styles.module.css b/src/stories/utils/styles.module.css index e25363c0..479057fe 100644 --- a/src/stories/utils/styles.module.css +++ b/src/stories/utils/styles.module.css @@ -1,3 +1,8 @@ +.content { + width: calc(100vw - 200px); + height: calc(100vh - 200px); +} + .controlPanel { position: absolute; z-index: 2; @@ -31,6 +36,4 @@ .grid { display: grid; grid-template-columns: repeat(100, 1fr); - width: calc(100vw - 200px); - height: calc(100vh - 200px); }