From 58469899c60f5b91312b10b66ae998f7f819cdf2 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 14:52:35 +0200 Subject: [PATCH 01/10] fix: switch the signature pad on and off while resizing, prevents broken lifecycle --- .../src/utils/useSignaturePad.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index e1d420f848..efb518ceef 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -58,14 +58,20 @@ export function useSignaturePad( }, [readOnly]); const onResize = (): void => { - if (canvasRef.current && signaturePadRef.current) { - const data = signaturePadRef.current.toData(); - canvasRef.current.width = - canvasRef.current && canvasRef.current.parentElement ? canvasRef.current.parentElement.offsetWidth : 0; - canvasRef.current.height = - canvasRef.current && canvasRef.current.parentElement ? canvasRef.current.parentElement.offsetHeight : 0; - signaturePadRef.current.clear(); - signaturePadRef.current.fromData(data); + const pad = signaturePadRef.current; + const canvas = canvasRef.current; + if (pad && canvas) { + const data = pad.toData(); + // off()+on() resets _drawingStroke and clears stale pointer/move listeners, + // preventing pointerdown from being silently dropped after a mid-stroke resize. + pad.off(); + canvas.width = canvas.parentElement?.offsetWidth ?? 0; + canvas.height = canvas.parentElement?.offsetHeight ?? 0; + pad.clear(); + pad.fromData(data); + if (!readOnly) { + pad.on(); + } } }; From 1c9ebba50a42608b62fceee18814bff3d3b0e100 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 14:53:18 +0200 Subject: [PATCH 02/10] fix: initialize canvas on the actual parent size --- .../signature-web/src/utils/useSignaturePad.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index efb518ceef..c0cbdfed3a 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -95,6 +95,13 @@ export function useSignaturePad( signaturePadRef.current === null && (imageSource?.status === "available" ? imageSource.value?.uri : imageSource.status === "unavailable"); if (canInstantiateSignaturePad && !isSignatureInitialized.current) { + // Set canvas dimensions to the actual parent size before initializing the pad. + // ResizeObserver may have already fired and been a no-op (pad was null then), + // so we can't rely on onResize to set the correct initial size. + if (localCanvas.parentElement) { + localCanvas.width = localCanvas.parentElement.offsetWidth; + localCanvas.height = localCanvas.parentElement.offsetHeight; + } signaturePadRef.current = new SignaturePad(localCanvas, { penColor, ...signaturePadOptions From c8ce0f2d0d421a33fb063ee6f34871f37c5ac027 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 14:56:07 +0200 Subject: [PATCH 03/10] chore: inline pen options --- .../src/utils/useSignaturePad.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index c0cbdfed3a..e4304e3d00 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -1,7 +1,18 @@ -import { RefObject, useCallback, useEffect, useMemo, useRef } from "react"; +import { RefObject, useCallback, useEffect, useRef } from "react"; import SignaturePad, { Options } from "signature_pad"; import { SignatureContainerProps } from "../../typings/SignatureProps"; +function getPenOptions(penType: string): Options { + if (penType === "fountain") { + return { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }; + } else if (penType === "ballpoint") { + return { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }; + } else if (penType === "marker") { + return { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 }; + } + return {}; +} + function usePrevious(value: T): T | null { const ref = useRef(null); useEffect(() => { @@ -25,18 +36,6 @@ export function useSignaturePad( const isSignatureInitialized = useRef(false); const hasSignature = usePrevious(hasSignatureAttribute?.value ?? false) ?? false; - const signaturePadOptions: Options = useMemo(() => { - let options: Options = {}; - if (penType === "fountain") { - options = { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }; - } else if (penType === "ballpoint") { - options = { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }; - } else if (penType === "marker") { - options = { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 }; - } - return options; - }, [penType]); - const handleSignEnd = useCallback(() => { const imageDataUrl = signaturePadRef.current?.toDataURL(); @@ -102,10 +101,7 @@ export function useSignaturePad( localCanvas.width = localCanvas.parentElement.offsetWidth; localCanvas.height = localCanvas.parentElement.offsetHeight; } - signaturePadRef.current = new SignaturePad(localCanvas, { - penColor, - ...signaturePadOptions - }); + signaturePadRef.current = new SignaturePad(localCanvas, { penColor, ...getPenOptions(penType) }); signaturePadRef.current.addEventListener("endStroke", handleSignEnd); if (readOnly) { signaturePadRef.current?.off(); @@ -113,7 +109,7 @@ export function useSignaturePad( isSignatureInitialized.current = true; } } - }, [handleSignEnd, penColor, readOnly, signaturePadOptions, imageSource, hasSignatureAttribute]); + }, [handleSignEnd, penColor, penType, readOnly, imageSource, hasSignatureAttribute]); return { signaturePadRef, canvasRef, onResize }; } From 9cc5d8cacf3147a9700aa66c53a0e6b5ce4d5b27 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 15:01:42 +0200 Subject: [PATCH 04/10] chore: use redraw instead of manual data manipulations --- .../signature-web/src/utils/useSignaturePad.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index e4304e3d00..2743151fe9 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -60,14 +60,12 @@ export function useSignaturePad( const pad = signaturePadRef.current; const canvas = canvasRef.current; if (pad && canvas) { - const data = pad.toData(); // off()+on() resets _drawingStroke and clears stale pointer/move listeners, // preventing pointerdown from being silently dropped after a mid-stroke resize. pad.off(); canvas.width = canvas.parentElement?.offsetWidth ?? 0; canvas.height = canvas.parentElement?.offsetHeight ?? 0; - pad.clear(); - pad.fromData(data); + pad.redraw(); if (!readOnly) { pad.on(); } From bced16ffb60fe1d5ae999c3f799fed7b6c5af16d Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 15:13:24 +0200 Subject: [PATCH 05/10] chore: safer type for pen options --- .../src/utils/useSignaturePad.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index 2743151fe9..48755d30ad 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -1,16 +1,15 @@ import { RefObject, useCallback, useEffect, useRef } from "react"; import SignaturePad, { Options } from "signature_pad"; -import { SignatureContainerProps } from "../../typings/SignatureProps"; +import { PenTypeEnum, SignatureContainerProps } from "../../typings/SignatureProps"; -function getPenOptions(penType: string): Options { - if (penType === "fountain") { - return { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }; - } else if (penType === "ballpoint") { - return { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }; - } else if (penType === "marker") { - return { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 }; - } - return {}; +const PEN_OPTIONS: Record = { + fountain: { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }, + ballpoint: { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }, + marker: { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 } +}; + +function getPenOptions(penType: PenTypeEnum): Options { + return PEN_OPTIONS[penType]; } function usePrevious(value: T): T | null { From e205a1894f6764ab6f88944cf573097c18a126d2 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 15:15:06 +0200 Subject: [PATCH 06/10] chore: improve readability and remove unnecessary variables and refs --- .../signature-web/src/utils/useSignaturePad.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index 48755d30ad..19faa7f80a 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -24,7 +24,6 @@ export function useSignaturePad( props: Pick, onSignEnd?: (imageDataURL?: string) => void ): { - signaturePadRef: RefObject; canvasRef: RefObject; onResize?: () => void; } { @@ -32,7 +31,6 @@ export function useSignaturePad( const readOnly = imageSource.readOnly; const signaturePadRef = useRef(null); const canvasRef = useRef(null); - const isSignatureInitialized = useRef(false); const hasSignature = usePrevious(hasSignatureAttribute?.value ?? false) ?? false; const handleSignEnd = useCallback(() => { @@ -73,12 +71,8 @@ export function useSignaturePad( // Clear signature pad when hasSignature value changes from true to false useEffect(() => { - if (hasSignatureAttribute?.status === "available") { - if (hasSignatureAttribute?.value !== hasSignature) { - if (hasSignature === true) { - signaturePadRef.current?.clear(); - } - } + if (hasSignatureAttribute?.status === "available" && hasSignature && hasSignatureAttribute.value === false) { + signaturePadRef.current?.clear(); } }, [hasSignature, hasSignatureAttribute?.status, hasSignatureAttribute?.value]); @@ -90,7 +84,7 @@ export function useSignaturePad( const canInstantiateSignaturePad = signaturePadRef.current === null && (imageSource?.status === "available" ? imageSource.value?.uri : imageSource.status === "unavailable"); - if (canInstantiateSignaturePad && !isSignatureInitialized.current) { + if (canInstantiateSignaturePad) { // Set canvas dimensions to the actual parent size before initializing the pad. // ResizeObserver may have already fired and been a no-op (pad was null then), // so we can't rely on onResize to set the correct initial size. @@ -101,12 +95,11 @@ export function useSignaturePad( signaturePadRef.current = new SignaturePad(localCanvas, { penColor, ...getPenOptions(penType) }); signaturePadRef.current.addEventListener("endStroke", handleSignEnd); if (readOnly) { - signaturePadRef.current?.off(); + signaturePadRef.current.off(); } - isSignatureInitialized.current = true; } } }, [handleSignEnd, penColor, penType, readOnly, imageSource, hasSignatureAttribute]); - return { signaturePadRef, canvasRef, onResize }; + return { canvasRef, onResize }; } From 3d921b0b85622d423556a9f71f255915fa0a4745 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 15:55:56 +0200 Subject: [PATCH 07/10] chore: make useSignaturePad own resize observer and expose refs outside --- .../src/components/Signature.tsx | 4 +- .../src/components/SizeContainer.tsx | 11 +-- .../src/utils/useSignaturePad.ts | 87 ++++++++++--------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/components/Signature.tsx b/packages/pluggableWidgets/signature-web/src/components/Signature.tsx index cc2e0ef595..908efae4e5 100644 --- a/packages/pluggableWidgets/signature-web/src/components/Signature.tsx +++ b/packages/pluggableWidgets/signature-web/src/components/Signature.tsx @@ -25,16 +25,16 @@ export function SignatureComponent(props: SignatureContainerProps): ReactElement } }; - const { canvasRef, onResize } = useSignaturePad(props, handleSignEnd); + const { canvasRef, containerRef } = useSignaturePad(props, handleSignEnd); return ( {validation && {validation}} diff --git a/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx b/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx index 5a1460bc4b..655acb042d 100644 --- a/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx +++ b/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx @@ -1,23 +1,21 @@ import classNames from "classnames"; -import { CSSProperties, FC, PropsWithChildren, RefObject, useMemo } from "react"; -import { useResizeObserver } from "@mendix/widget-plugin-hooks/useResizeObserver"; +import { CSSProperties, ForwardedRef, forwardRef, PropsWithChildren, useMemo } from "react"; import { constructWrapperStyle, DimensionsProps } from "../utils/dimensions"; + export interface SizeProps extends DimensionsProps, PropsWithChildren { className: string; classNameInner?: string; readOnly?: boolean; style?: CSSProperties; - onResize?: () => void; tabIndex?: number; } -export const SizeContainer: FC = (props: SizeProps) => { +export const SizeContainer = forwardRef(function SizeContainer(props: SizeProps, ref: ForwardedRef) { const { className, children, classNameInner, readOnly = false, - onResize, widthUnit, width, heightUnit, @@ -29,7 +27,6 @@ export const SizeContainer: FC = (props: SizeProps) => { overflowY, tabIndex } = props; - const ref = useResizeObserver(() => onResize?.()) as RefObject; const wrapperStyle = useMemo( () => constructWrapperStyle({ @@ -60,4 +57,4 @@ export const SizeContainer: FC = (props: SizeProps) => { ); -}; +}); diff --git a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts index 19faa7f80a..06fbba4d01 100644 --- a/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts +++ b/packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts @@ -1,31 +1,14 @@ import { RefObject, useCallback, useEffect, useRef } from "react"; import SignaturePad, { Options } from "signature_pad"; +import { useResizeObserver } from "@mendix/widget-plugin-hooks/useResizeObserver"; import { PenTypeEnum, SignatureContainerProps } from "../../typings/SignatureProps"; -const PEN_OPTIONS: Record = { - fountain: { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }, - ballpoint: { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }, - marker: { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 } -}; - -function getPenOptions(penType: PenTypeEnum): Options { - return PEN_OPTIONS[penType]; -} - -function usePrevious(value: T): T | null { - const ref = useRef(null); - useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} - export function useSignaturePad( props: Pick, onSignEnd?: (imageDataURL?: string) => void ): { canvasRef: RefObject; - onResize?: () => void; + containerRef: RefObject; } { const { imageSource, hasSignatureAttribute, penType, penColor } = props; const readOnly = imageSource.readOnly; @@ -35,7 +18,6 @@ export function useSignaturePad( const handleSignEnd = useCallback(() => { const imageDataUrl = signaturePadRef.current?.toDataURL(); - if (hasSignatureAttribute) { hasSignatureAttribute.setValue(!signaturePadRef.current?.isEmpty()); } @@ -53,21 +35,26 @@ export function useSignaturePad( } }, [readOnly]); - const onResize = (): void => { - const pad = signaturePadRef.current; - const canvas = canvasRef.current; - if (pad && canvas) { - // off()+on() resets _drawingStroke and clears stale pointer/move listeners, - // preventing pointerdown from being silently dropped after a mid-stroke resize. - pad.off(); - canvas.width = canvas.parentElement?.offsetWidth ?? 0; - canvas.height = canvas.parentElement?.offsetHeight ?? 0; - pad.redraw(); - if (!readOnly) { - pad.on(); + const handleResize = useCallback( + (element: HTMLDivElement) => { + const pad = signaturePadRef.current; + const canvas = canvasRef.current; + if (pad && canvas) { + // off()+on() resets _drawingStroke and clears stale pointer/move listeners, + // preventing pointerdown from being silently dropped after a mid-stroke resize. + pad.off(); + canvas.width = element.offsetWidth; + canvas.height = element.offsetHeight; + pad.redraw(); + if (!readOnly) { + pad.on(); + } } - } - }; + }, + [readOnly] + ); + + const containerRef = useResizeObserver(handleResize) as RefObject; // Clear signature pad when hasSignature value changes from true to false useEffect(() => { @@ -85,12 +72,10 @@ export function useSignaturePad( signaturePadRef.current === null && (imageSource?.status === "available" ? imageSource.value?.uri : imageSource.status === "unavailable"); if (canInstantiateSignaturePad) { - // Set canvas dimensions to the actual parent size before initializing the pad. - // ResizeObserver may have already fired and been a no-op (pad was null then), - // so we can't rely on onResize to set the correct initial size. - if (localCanvas.parentElement) { - localCanvas.width = localCanvas.parentElement.offsetWidth; - localCanvas.height = localCanvas.parentElement.offsetHeight; + const container = containerRef.current; + if (container) { + localCanvas.width = container.offsetWidth; + localCanvas.height = container.offsetHeight; } signaturePadRef.current = new SignaturePad(localCanvas, { penColor, ...getPenOptions(penType) }); signaturePadRef.current.addEventListener("endStroke", handleSignEnd); @@ -99,7 +84,25 @@ export function useSignaturePad( } } } - }, [handleSignEnd, penColor, penType, readOnly, imageSource, hasSignatureAttribute]); + }, [handleSignEnd, penColor, penType, readOnly, imageSource, hasSignatureAttribute, containerRef]); + + return { canvasRef, containerRef }; +} + +const PEN_OPTIONS: Record = { + fountain: { minWidth: 0.6, maxWidth: 2.6, velocityFilterWeight: 0.6 }, + ballpoint: { minWidth: 1.4, maxWidth: 1.5, velocityFilterWeight: 1.5 }, + marker: { minWidth: 2, maxWidth: 4, velocityFilterWeight: 0.9 } +}; - return { canvasRef, onResize }; +function getPenOptions(penType: PenTypeEnum): Options { + return PEN_OPTIONS[penType]; +} + +function usePrevious(value: T): T | null { + const ref = useRef(null); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; } From 68cf925b49cf5549bae2caa7d88408b234144adb Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 7 Aug 2026 16:04:15 +0200 Subject: [PATCH 08/10] chore: add changelogs --- packages/pluggableWidgets/signature-web/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/pluggableWidgets/signature-web/CHANGELOG.md b/packages/pluggableWidgets/signature-web/CHANGELOG.md index eacc7ef12d..9c5c216980 100644 --- a/packages/pluggableWidgets/signature-web/CHANGELOG.md +++ b/packages/pluggableWidgets/signature-web/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue where strokes stopped being registered in some cases. + +- We fixed an issue where the signature canvas was initialized at the wrong size and did not fill its container. + ## [2.0.1] - 2026-07-17 ### Fixed From e17b78a98ec61ba5e6c0335aca8c4749ca13e18e Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 11 Aug 2026 15:44:33 +0200 Subject: [PATCH 09/10] chore: remove useless inner class prop prop --- .../signature-web/src/Signature.editorPreview.tsx | 3 --- .../signature-web/src/components/Signature.tsx | 3 --- .../signature-web/src/components/SizeContainer.tsx | 14 +++++++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/pluggableWidgets/signature-web/src/Signature.editorPreview.tsx b/packages/pluggableWidgets/signature-web/src/Signature.editorPreview.tsx index d1837f4c0e..62e9656c88 100644 --- a/packages/pluggableWidgets/signature-web/src/Signature.editorPreview.tsx +++ b/packages/pluggableWidgets/signature-web/src/Signature.editorPreview.tsx @@ -26,9 +26,6 @@ export function preview(props: SignaturePreviewProps): ReactElement { return ( {validation && {validation}} diff --git a/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx b/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx index 655acb042d..64835e3d4b 100644 --- a/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx +++ b/packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx @@ -4,7 +4,6 @@ import { constructWrapperStyle, DimensionsProps } from "../utils/dimensions"; export interface SizeProps extends DimensionsProps, PropsWithChildren { className: string; - classNameInner?: string; readOnly?: boolean; style?: CSSProperties; tabIndex?: number; @@ -14,7 +13,6 @@ export const SizeContainer = forwardRef(function SizeContainer(props: SizeProps, const { className, children, - classNameInner, readOnly = false, widthUnit, width, @@ -52,7 +50,17 @@ export const SizeContainer = forwardRef(function SizeContainer(props: SizeProps, }} tabIndex={tabIndex} > -
+
{children}
From bc2d955085136c905778b9800fcc1eff464c20eb Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 11 Aug 2026 15:59:32 +0200 Subject: [PATCH 10/10] chore: add unit test --- .../src/__tests__/useSignaturePad.spec.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx diff --git a/packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx b/packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx new file mode 100644 index 0000000000..60047e1e89 --- /dev/null +++ b/packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx @@ -0,0 +1,104 @@ +import "@testing-library/jest-dom"; +import { act, render } from "@testing-library/react"; +import { ReactElement } from "react"; +import SignaturePad from "signature_pad"; +import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; +import { SignatureContainerProps } from "../../typings/SignatureProps"; +import { useSignaturePad } from "../utils/useSignaturePad"; + +type ImageSource = SignatureContainerProps["imageSource"]; + +jest.mock("signature_pad", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(function (this: any) { + this.on = jest.fn(); + this.off = jest.fn(); + this.redraw = jest.fn(); + this.addEventListener = jest.fn(); + this.isEmpty = jest.fn(() => true); + }) +})); + +const MockSignaturePad = SignaturePad as jest.MockedClass; + +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn() +})); + +function buildImageSource(overrides: Partial = {}): ImageSource { + return { + ...new EditableValueBuilder().isUnavailable().build(), + ...overrides + } as unknown as ImageSource; +} + +// Wrapper component that mounts both refs into real DOM nodes +function TestHarness({ imageSource }: { imageSource: ImageSource }): ReactElement { + const { containerRef, canvasRef } = useSignaturePad({ imageSource, penType: "ballpoint", penColor: "#000000" }); + return ( +
+ +
+ ); +} + +describe("useSignaturePad — canvas initialization", () => { + // jsdom doesn't do layout, so offsetWidth/Height are always 0. + // Stub them on the prototype before each test so the init effect reads real numbers. + let offsetWidthSpy: jest.SpyInstance; + let offsetHeightSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + offsetWidthSpy?.mockRestore(); + offsetHeightSpy?.mockRestore(); + }); + + function stubContainerDimensions(width: number, height: number): void { + offsetWidthSpy = jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(width); + offsetHeightSpy = jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(height); + } + + it("sizes canvas to containerRef dimensions when imageSource is unavailable", () => { + stubContainerDimensions(400, 200); + const imageSource = buildImageSource(); + + const { getByTestId } = render(); + + const canvas = getByTestId("canvas") as HTMLCanvasElement; + expect(canvas.width).toBe(400); + expect(canvas.height).toBe(200); + expect(MockSignaturePad).toHaveBeenCalledWith(canvas, expect.any(Object)); + }); + + it("sizes canvas to containerRef dimensions when imageSource is available with a value", () => { + stubContainerDimensions(600, 300); + const imageSource = buildImageSource({ + status: "available" as any, + value: { uri: "data:image/png;base64,abc" } as any, + readOnly: false + }); + + const { getByTestId } = render(); + + const canvas = getByTestId("canvas") as HTMLCanvasElement; + expect(canvas.width).toBe(600); + expect(canvas.height).toBe(300); + expect(MockSignaturePad).toHaveBeenCalledWith(canvas, expect.any(Object)); + }); + + it("does not initialize SignaturePad when imageSource is still loading", () => { + const imageSource = buildImageSource({ status: "loading" as any, readOnly: true }); + + render(); + + act(() => {}); + + expect(MockSignaturePad).not.toHaveBeenCalled(); + }); +});