Issue with scrolling

Product

@pdftron/webviewer

Product Version

^11.2.0

Summary

WebViewer doesn’t respond to mouse wheel/trackpad scrolling while scroll bars are visible

Description

The WebViewer component shows scroll bars and can be scrolled using the scroll bar directly, but mouse wheel and trackpad scrolling gestures do not work. This creates a poor user experience as users have to manually drag the scroll bar instead of using natural scrolling inputs.

Steps to Reproduce

  1. Create a React component that implements WebViewer
  2. Load a document that is longer than the viewport
  3. Observe that scroll bars appear correctly
  4. Try to scroll using:
    • Mouse wheel (doesn’t work)
    • Trackpad gestures (doesn’t work)
    • Scroll bar dragging (works)

Code Sample

import React, { useEffect, useRef, useState } from "react";
import { trpc } from "lib/trpc/client";
import { toast } from "sonner";
import {
  useIsMutating,
  useMutation,
  useQueryClient,
} from "@tanstack/react-query";
import { handleNewVersionUpload } from "lib/components/files/handleFileUpload";

interface DocumentViewerProps {
  contentID: number;
  onClose?: () => void;
}

interface DocumentData {
  url: string;
  fileName: string;
  fileType: string;
  isImage: boolean;
  isOfficeDoc: boolean;
}

const DocumentViewer: React.FC<DocumentViewerProps> = ({
  contentID: contentID,
  onClose,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const viewerInstance = useRef<any>(null);
  const [documentData, setDocumentData] = useState<DocumentData | null>(null);
  const blobUrlRef = useRef<string | null>(null);
  const elementRef = useRef<HTMLDivElement | null>(null);
  const queryClient = useQueryClient();

  const webviewerServerUrl = trpc.documentViewer.getDocumentViewerServerUrl.useQuery().data;
  const licenseKey = trpc.documentViewer.getDocumentViewerLicenseKey.useQuery().data;
  const appInstance = trpc.instance.getInstance.useQuery().data;

  const isSaving = useIsMutating({
    mutationKey: ["content", "createNewVersion"],
  });

  const { data: metadata } =
    trpc.content.getPreferredRenditionMetadataForLatestVersionOfContent.useQuery(
      contentID,
      {
        enabled: !!contentID,
      }
    );

  const { data: presignedUrl } =
    trpc.content.getPresignedUrlForLatestVersionForContent.useQuery(contentID, {
      enabled: !!contentID && !!metadata,
    });

  const createVersionMutation = trpc.content.createNewVersion.useMutation({
    mutationKey: ["content", "createNewVersion"],
    onSuccess: async () => {
      queryClient.invalidateQueries({
        queryKey: ["filemeta"],
      });
      queryClient.invalidateQueries({
        predicate: (query) => {
          const key = query.queryKey[0];
          return typeof key === "string" && key.startsWith("fileview/preview/");
        },
      });
    },
    onError: (error) => {
      toast("Fehler beim Speichern der Version", { position: "bottom-right" });
      console.error("Fehler beim Speichern der Version:", error);
      setError("Version konnte nicht gespeichert werden");
    },
  });

  const uploadVersionMutation = useMutation({
    mutationFn: handleNewVersionUpload,
    onSuccess: () => {
      toast("Version erfolgreich gespeichert", { position: "bottom-right" });
    },
  });

  const expireContentMutation = trpc.content.expireContent.useMutation({
    onSuccess: () => {
      toast("Dokument erfolgreich gelöscht", { position: "bottom-right" });
      queryClient.invalidateQueries({ queryKey: ["filemeta"] });
      onClose?.();
    },
    onError: (error) => {
      toast("Fehler beim Löschen des Dokuments", { position: "bottom-right" });
      console.error("Error deleting document:", error);
    },
  });

  const handleDelete = async () => {
    try {
      await expireContentMutation.mutateAsync(contentID);
    } catch (error) {
      console.error("Delete error:", error);
    }
  };

  const handleDownload = async () => {
    try {
      if (!documentData?.url || !documentData?.fileName) return;

      const response = await fetch(documentData.url);
      const blob = await response.blob();
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = documentData.fileName;
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
    } catch (error) {
      console.error("Download error:", error);
      toast("Fehler beim Herunterladen der Datei", {
        position: "bottom-right",
      });
    }
  };

  useEffect(() => {
    if (presignedUrl && metadata) {
      const isImage = metadata.fileType === 'image/jpg' || metadata.fileType.startsWith("image/");
      const isOfficeDoc = metadata.fileType.startsWith("application/vnd.openxmlformats-officedocument");
      setDocumentData({
        url: presignedUrl,
        fileName: metadata.fileName,
        fileType: metadata.fileType,
        isImage,
        isOfficeDoc
      });
    }
  }, [presignedUrl, metadata]);

  useEffect(() => {
    if (!documentData?.url || !containerRef.current) return;

    const cleanupViewer = async () => {
      if (viewerInstance.current) {
        try {
          await viewerInstance.current.UI.closeDocument();
          await viewerInstance.current.UI.dispose();
        } catch (err) {
          console.error("Error cleaning up viewer:", err);
        }
        viewerInstance.current = null;
      }
    };

    const initializeViewer = async () => {
      try {
        await cleanupViewer();

        if (elementRef.current) {
          elementRef.current.remove();
        }
        elementRef.current = document.createElement("div");
        elementRef.current.className = "h-full";
        containerRef.current?.appendChild(elementRef.current);

        const WebViewer = (await import("@pdftron/webviewer")).default;

        const instance = await WebViewer(
          {
            path: "/webviewer/lib",
            initialDoc: `${process.env.NODE_ENV === "production"
              ? `https://${appInstance}.test.pp`
              : ""
              }/api/files/${documentData.fileName}?url=${encodeURIComponent(
                documentData.url
              )}&fileType=${encodeURIComponent(documentData.fileType)}`,
            licenseKey: licenseKey || "",
            webviewerServerURL:
              !documentData.isImage &&
                documentData.fileType !== "application/pdf"
                ? webviewerServerUrl || ""
                : "",
            enableFilePicker: false,
            fullAPI: true,
            enableAnnotations: true,
            useDownloader: true,
            backendType: "ems",
            streaming: true,
            loadAsPDF: false,
            enableOfficeEditing: documentData.isOfficeDoc,
            ...(documentData.isImage && {
              imageToolsEnabled: true,
              enableImageTools: true,
              enableImageEditing: true,
            }),
          },
          elementRef.current
        );

        viewerInstance.current = instance;
        setLoading(false);

        instance.Core.documentViewer.addEventListener("documentLoaded", () => {
          instance.UI.setLanguage("de");

          const testFlyoutButton = {
            dataElement: "downloadButton",
            label: "Download",
            onClick: handleDownload,
            icon: "icon-download",
          };
          const mainMenuFlyout =
            instance.UI.Flyouts.getFlyout("MainMenuFlyout");
          const mainMenuFlyoutItems = mainMenuFlyout.items;
          const downloadButtonIndex = mainMenuFlyoutItems.findIndex(
            (item: any) => item.dataElement === "downloadButton"
          );
          mainMenuFlyoutItems[downloadButtonIndex] = testFlyoutButton;
          mainMenuFlyout.setItems(mainMenuFlyoutItems);

          // Set active ribbon item using new Modular UI
          instance.UI.setActiveRibbonItem("toolbarGroup-Annotate");

          if (documentData.isImage) {
            const imageTools = [
              "cropToolButton",
              "rotateToolButton",
              "filterToolButton",
              "eraserToolButton",
              "freeHandToolButton",
              "rectangleToolButton",
              "ellipseToolButton",
              "lineToolButton",
              "arrowToolButton",
              "textToolButton",
            ];

            // Enable tools using Modular UI
            imageTools.forEach((tool) => {
              instance.UI.enableElements([tool]);
            });
          }

          instance.UI.setHeaderItems(() => {
            const header = instance.UI.getModularHeader("default-top-header");
            if (documentData.fileType === 'application/pdf' || documentData.isOfficeDoc) {
              header.setItems([
                ...header.getItems(),
                {
                  type: "customButton",
                  dataElement: "saveVersionButton",
                  img: "/icons/icon-save.svg",
                  onClick: async () => {
                    try {
                      console.log(metadata);
                      if (!metadata?.versionId) {
                        setError("Version-ID konnte nicht ermittelt werden");
                        return;
                      }

                      const doc = instance.Core.documentViewer.getDocument();
                      const xfdfString =
                        await instance.Core.annotationManager.exportAnnotations();
                      const data = await doc.getFileData({ xfdfString });
                      const fileData = new Blob([data], {
                        type: documentData.fileType,
                      });

                      const response = await createVersionMutation.mutateAsync({
                        contentID,
                        content: {
                          fileName: documentData.fileName,
                          fileSize: fileData.size,
                          fileType: documentData.fileType,
                          isPublic: false,
                          providerID: "s3",
                        },
                      });

                      if (!response) {
                        setError("Upload-URL konnte nicht abgerufen werden");
                        return;
                      }

                      await uploadVersionMutation.mutateAsync({
                        uploadUrl: response.uploadUrl,
                        data: fileData,
                        fileType: documentData.fileType,
                      });
                    } catch (error) {
                      console.error("Fehler beim Speichern der Version:", error);
                      setError("Version konnte nicht gespeichert werden");
                    }
                  },
                },
              ]);
            }

            header.setItems([
              ...header.getItems(),
              {
                type: "customButton",
                img: "/icons/icon-delete.svg",
                onClick: handleDelete,
                title: "Dokument löschen",
                dataElement: "deleteButton",
              }
            ]);
          });

          instance.UI.setToolbarGroup("toolbarGroup-Annotate");

          if (documentData.isImage) {
            instance.UI.enableElements([
              "cropToolButton",
              "rotateToolButton",
              "filterToolButton",
              "eraserToolButton",
              "freeHandToolButton",
              "rectangleToolButton",
              "ellipseToolButton",
              "lineToolButton",
              "arrowToolButton",
              "textToolButton",
            ]);
          }

          instance.UI.openElements(["header"]);
          setLoading(false);
        });

        instance.Core.documentViewer.addEventListener(
          "error",
          (error: Error) => {
            setError(error.message);
            setLoading(false);
          }
        );

        const styleSheet = document.createElement("style");
        styleSheet.textContent = `
          .Header { 
            display: flex !important;
            visibility: visible !important;
          }
        `;
        document.head.appendChild(styleSheet);
      } catch (error) {
        console.error("Viewer-Fehler:", error);
        setError("Viewer konnte nicht initialisiert werden");
        setLoading(false);
      }
    };

    initializeViewer();

    return () => {
      cleanupViewer();
      if (blobUrlRef.current) {
        URL.revokeObjectURL(blobUrlRef.current.split("#")[0]);
        blobUrlRef.current = null;
      }
      if (elementRef.current) {
        elementRef.current.remove();
      }
    };
  }, [documentData, contentID, metadata]);

  if (!documentData) {
    return (
      <div className="flex items-center justify-center h-full">
        <p className="text-gray-600">Dokument wird geladen...</p>
      </div>
    );
  }

  return (
    <div className="w-full h-full relative">
      {(loading || isSaving > 0) && (
        <div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-50 z-50">
          <p className="text-gray-600">
            {isSaving > 0 ? "Speichern..." : "Dokument wird geladen..."}
          </p>
        </div>
      )}
      {error && (
        <div className="absolute inset-0 flex items-center justify-center bg-white">
          <p className="text-red-500">{error}</p>
        </div>
      )}
      <div
        ref={containerRef}
        className="h-full"
        style={{ isolation: "isolate" }}
      />
    </div>
  );
};

export default DocumentViewer;

Current Behavior

  • Scroll bars are visible when content exceeds container height
  • Can scroll by dragging the scroll bar manually
  • Cannot scroll using mouse wheel
  • Cannot scroll using trackpad gestures
  • No response to keyboard scrolling inputs (Page Up/Down, arrow keys)

Expected Behavior

  • Should be able to scroll using mouse wheel
  • Should be able to scroll using trackpad gestures
  • Should be able to scroll using keyboard inputs
  • Should maintain the ability to scroll using scroll bars

Environment

  • Browser: Chrome latest version
  • OS: Windows/Mac
  • React Version: 18.x
  • TypeScript Version: 5.x

Additional Notes

  • The container has proper height settings and overflow properties
  • Using Tailwind CSS for styling
  • The issue persists across different document types (PDF, Office docs, images)
  • Scroll bars are correctly displayed and functional when dragged manually
  • Have tried setting ScrollMode.Continuous in the UI options

Attempted Solutions

  1. Added overflow-auto to container
  2. Tried different scroll mode settings
  3. Verified event listeners are properly attached
  4. Checked for any CSS properties that might be blocking scroll events
1 Like

Hi Patrick.

To investigate the issue, could you please provide a minimal sample project and the sample file for us to reproduce the issue?

You can find base samples here: Free Trial: JavaScript PDF Viewer | Apryse documentation

Best regards,
Mickaël.

1 Like

Hi Patrick,

We haven’t heard back from you for several days, is your issue resolved?

Best Regards,
Mickaël

1 Like