mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
fix: pdf viewer scroll elements
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -38,6 +40,8 @@ export const DocumentDuplicateDialog = ({
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
|
||||
trpcReact.envelope.item.getManyByToken.useQuery(
|
||||
{
|
||||
@@ -95,12 +99,13 @@ export const DocumentDuplicateDialog = ({
|
||||
</h1>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
|
||||
<div ref={scrollContainerRef} className="h-[50vh] overflow-y-scroll p-2">
|
||||
<PDFViewer
|
||||
key={envelopeItems[0].id}
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="initial"
|
||||
scrollParentRef={scrollContainerRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -551,6 +551,7 @@ export const ConfigureFieldsView = ({
|
||||
envelopeItem={normalizedEnvelopeItem}
|
||||
token={undefined}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
|
||||
<ElementVisible
|
||||
|
||||
@@ -345,6 +345,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={recipient.token}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -291,6 +291,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={token}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@@ -66,6 +66,8 @@ export const MultiSignDocumentSigningView = ({
|
||||
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
@@ -179,7 +181,11 @@ export const MultiSignDocumentSigningView = ({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen overflow-hidden bg-background">
|
||||
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
|
||||
<div
|
||||
id="document-field-portal-root"
|
||||
ref={scrollContainerRef}
|
||||
className="relative h-full w-full overflow-y-auto p-8"
|
||||
>
|
||||
{match({ isLoading, document })
|
||||
.with({ isLoading: true }, () => (
|
||||
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||
@@ -230,6 +236,7 @@ export const MultiSignDocumentSigningView = ({
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={token}
|
||||
version="current"
|
||||
scrollParentRef={scrollContainerRef}
|
||||
onDocumentLoad={() => {
|
||||
setHasDocumentLoaded(true);
|
||||
onDocumentReady?.();
|
||||
|
||||
@@ -156,6 +156,7 @@ export const DirectTemplatePageView = ({
|
||||
envelopeItem={template.envelopeItems[0]}
|
||||
token={directTemplateRecipient.token}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -279,6 +279,7 @@ export const DocumentSigningPageViewV1 = ({
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={recipient.token}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+8
-2
@@ -1,4 +1,4 @@
|
||||
import { lazy, useMemo } from 'react';
|
||||
import { lazy, useMemo, useRef } from 'react';
|
||||
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
@@ -41,6 +41,8 @@ const EnvelopeSignerPageRenderer = lazy(
|
||||
export const DocumentSigningPageViewV2 = () => {
|
||||
const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
isDirectTemplate,
|
||||
envelope,
|
||||
@@ -200,7 +202,10 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="embed--DocumentContainer flex-1 overflow-y-auto">
|
||||
<div
|
||||
className="embed--DocumentContainer flex-1 overflow-y-auto"
|
||||
ref={scrollableContainerRef}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{/* Horizontal envelope item selector */}
|
||||
{envelopeItems.length > 1 && (
|
||||
@@ -232,6 +237,7 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
<EnvelopePdfViewer
|
||||
key={currentEnvelopeItem.id}
|
||||
customPageRenderer={EnvelopeSignerPageRenderer}
|
||||
scrollParentRef={scrollableContainerRef}
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.signing}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lazy, useEffect, useState } from 'react';
|
||||
import { lazy, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||
@@ -157,6 +157,7 @@ export const DocumentCertificateQRView = ({
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={token}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -180,6 +181,8 @@ const DocumentCertificateQrV2 = ({
|
||||
}: DocumentCertificateQrV2Props) => {
|
||||
const { envelopeItems } = useCurrentEnvelopeRender();
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-start">
|
||||
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
|
||||
@@ -210,11 +213,12 @@ const DocumentCertificateQrV2 = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 w-full">
|
||||
<div className="mt-12 max-h-[80vh] w-full overflow-y-auto" ref={scrollableContainerRef}>
|
||||
<EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
|
||||
|
||||
<EnvelopePdfViewer
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
scrollParentRef={scrollableContainerRef}
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -445,6 +445,7 @@ export const DocumentEditForm = ({
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lazy, useEffect, useMemo, useState } from 'react';
|
||||
import { lazy, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { faker } from '@faker-js/faker/locale/en';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -34,6 +34,8 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
|
||||
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
|
||||
'recipient',
|
||||
);
|
||||
@@ -214,7 +216,7 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
}}
|
||||
>
|
||||
<div className="relative flex h-full">
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
<div className="flex w-full flex-col overflow-y-auto" ref={scrollableContainerRef}>
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
|
||||
@@ -232,6 +234,7 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
{currentEnvelopeItem !== null ? (
|
||||
<EnvelopePdfViewer
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
scrollParentRef={scrollableContainerRef}
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -317,6 +317,7 @@ export const TemplateEditForm = ({
|
||||
envelopeItem={template.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -173,6 +173,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
||||
<CardContent className="p-2">
|
||||
<EnvelopePdfViewer
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
scrollParentRef="window"
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -200,6 +201,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
||||
token={undefined}
|
||||
key={envelope.envelopeItems[0].id}
|
||||
version="current"
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -191,6 +191,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
||||
<CardContent className="p-2">
|
||||
<EnvelopePdfViewer
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
scrollParentRef="window"
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -217,6 +218,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
||||
token={undefined}
|
||||
version="current"
|
||||
key={envelope.envelopeItems[0].id}
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -13,12 +13,22 @@ import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
|
||||
import type { ScrollTarget } from '../virtual-list/use-virtual-list';
|
||||
import { useVirtualList } from '../virtual-list/use-virtual-list';
|
||||
import { PdfViewerErrorState, PdfViewerLoadingState } from './pdf-viewer-states';
|
||||
|
||||
export type EnvelopePdfViewerProps = {
|
||||
className?: string;
|
||||
scrollParentRef?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
/**
|
||||
* Ref to the scrollable parent container that handles scrolling.
|
||||
*
|
||||
* This must point to an element with `overflow-y: auto` or `overflow-y: scroll`
|
||||
* that is an ancestor of this component, or `'window'` to use the browser
|
||||
* window as the scroll container.
|
||||
*/
|
||||
scrollParentRef: ScrollTarget;
|
||||
|
||||
onDocumentLoad?: () => void;
|
||||
|
||||
/**
|
||||
@@ -101,7 +111,7 @@ export const EnvelopePdfViewer = ({
|
||||
currentItemMeta &&
|
||||
numPages > 0 && (
|
||||
<VirtualizedPageList
|
||||
scrollParentRef={scrollParentRef ?? $el}
|
||||
scrollParentRef={scrollParentRef}
|
||||
constraintRef={$el}
|
||||
pages={currentItemMeta}
|
||||
envelopeItemId={currentEnvelopeItem.id}
|
||||
@@ -127,7 +137,7 @@ export const EnvelopePdfViewer = ({
|
||||
};
|
||||
|
||||
type VirtualizedPageListProps = {
|
||||
scrollParentRef: React.RefObject<HTMLDivElement>;
|
||||
scrollParentRef: ScrollTarget;
|
||||
constraintRef: React.RefObject<HTMLDivElement>;
|
||||
pages: BasePageRenderData[];
|
||||
envelopeItemId: string;
|
||||
@@ -144,9 +154,12 @@ const VirtualizedPageList = ({
|
||||
numPages,
|
||||
CustomPageRenderer,
|
||||
}: VirtualizedPageListProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { virtualItems, totalSize, constraintWidth } = useVirtualList({
|
||||
scrollRef: scrollParentRef,
|
||||
constraintRef,
|
||||
contentRef,
|
||||
itemCount: numPages,
|
||||
itemSize: (index, width) => {
|
||||
const pageMeta = pages[index];
|
||||
@@ -163,6 +176,7 @@ const VirtualizedPageList = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
width: '100%',
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { TGetEnvelopeItemsMetaResponse } from '@documenso/remix/server/api/
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useToast } from '../../primitives/use-toast';
|
||||
import type { ScrollTarget } from '../virtual-list/use-virtual-list';
|
||||
import { useVirtualList } from '../virtual-list/use-virtual-list';
|
||||
import { PdfViewerErrorState, PdfViewerLoadingState } from './pdf-viewer-states';
|
||||
|
||||
@@ -49,6 +50,16 @@ export type PDFViewerProps = {
|
||||
token: string | undefined;
|
||||
presignToken?: string | undefined;
|
||||
version: DocumentDataVersion;
|
||||
|
||||
/**
|
||||
* Ref to the scrollable parent container that handles scrolling.
|
||||
*
|
||||
* This must point to an element with `overflow-y: auto` or `overflow-y: scroll`
|
||||
* that is an ancestor of this component, or `'window'` to use the browser
|
||||
* window as the scroll container.
|
||||
*/
|
||||
scrollParentRef: ScrollTarget;
|
||||
|
||||
onDocumentLoad?: () => void;
|
||||
overrideImages?: OverrideImage[];
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
@@ -59,6 +70,7 @@ export const PDFViewer = ({
|
||||
token,
|
||||
presignToken,
|
||||
version,
|
||||
scrollParentRef,
|
||||
onDocumentLoad,
|
||||
overrideImages,
|
||||
...props
|
||||
@@ -168,7 +180,7 @@ export const PDFViewer = ({
|
||||
const hasError = loadingState === 'error';
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('h-full w-full overflow-hidden', className)} {...props}>
|
||||
<div ref={$el} className={cn('w-full', className)} {...props}>
|
||||
{/* Loading State */}
|
||||
{isLoading && <PdfViewerLoadingState />}
|
||||
|
||||
@@ -178,7 +190,7 @@ export const PDFViewer = ({
|
||||
{/* Loaded State */}
|
||||
{loadingState === 'loaded' && numPages > 0 && (
|
||||
<VirtualizedPageList
|
||||
scrollParentRef={$el}
|
||||
scrollParentRef={scrollParentRef}
|
||||
constraintRef={$el}
|
||||
numPages={numPages}
|
||||
pages={derivedPages}
|
||||
@@ -189,7 +201,7 @@ export const PDFViewer = ({
|
||||
};
|
||||
|
||||
type VirtualizedPageListProps = {
|
||||
scrollParentRef: React.RefObject<HTMLDivElement>;
|
||||
scrollParentRef: ScrollTarget;
|
||||
constraintRef: React.RefObject<HTMLDivElement>;
|
||||
pages: PageMeta[];
|
||||
numPages: number;
|
||||
@@ -203,9 +215,12 @@ const VirtualizedPageList = ({
|
||||
pages,
|
||||
numPages,
|
||||
}: VirtualizedPageListProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { virtualItems, totalSize, constraintWidth } = useVirtualList({
|
||||
scrollRef: scrollParentRef,
|
||||
constraintRef,
|
||||
contentRef,
|
||||
itemCount: numPages,
|
||||
itemSize: (index, width) => {
|
||||
const pageMeta = pages[index];
|
||||
@@ -222,6 +237,7 @@ const VirtualizedPageList = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
width: '100%',
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export type ScrollTarget = React.RefObject<HTMLElement | null> | 'window';
|
||||
|
||||
export type VirtualListOptions = {
|
||||
scrollRef: React.RefObject<HTMLElement | null>;
|
||||
scrollRef: ScrollTarget;
|
||||
constraintRef?: React.RefObject<HTMLElement | null>;
|
||||
|
||||
/**
|
||||
* Ref to the element that contains the virtual list content.
|
||||
*
|
||||
* Used to calculate the offset between the scroll container and the virtual
|
||||
* list when the scroll container is a parent element higher in the DOM tree.
|
||||
*
|
||||
* When the virtual list is not at the top of the scroll container (e.g. there
|
||||
* are headers, alerts, or other content above it), this offset ensures the
|
||||
* scroll position is correctly adjusted for virtualization calculations.
|
||||
*/
|
||||
contentRef?: React.RefObject<HTMLElement | null>;
|
||||
|
||||
itemCount: number;
|
||||
itemSize: number | ((index: number, constraintWidth: number) => number);
|
||||
overscan?: number;
|
||||
@@ -28,12 +43,20 @@ export type VirtualListResult = {
|
||||
* @returns Virtual items to render, total size, and constraint width
|
||||
*/
|
||||
export const useVirtualList = (options: VirtualListOptions): VirtualListResult => {
|
||||
const { scrollRef, constraintRef, itemCount, itemSize, overscan = 3 } = options;
|
||||
const { scrollRef, constraintRef, contentRef, itemCount, itemSize, overscan = 3 } = options;
|
||||
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const [viewportHeight, setViewportHeight] = useState(0);
|
||||
const [constraintWidth, setConstraintWidth] = useState(0);
|
||||
|
||||
/**
|
||||
* The offset of the content element relative to the scroll container.
|
||||
*
|
||||
* This is recalculated on scroll to handle cases where dynamic content
|
||||
* above the virtual list changes size.
|
||||
*/
|
||||
const contentOffsetRef = useRef(0);
|
||||
|
||||
// Track constraint element width with ResizeObserver
|
||||
useEffect(() => {
|
||||
const el = constraintRef?.current;
|
||||
@@ -60,6 +83,19 @@ export const useVirtualList = (options: VirtualListOptions): VirtualListResult =
|
||||
|
||||
// Track scroll container dimensions with ResizeObserver
|
||||
useEffect(() => {
|
||||
if (scrollRef === 'window') {
|
||||
const handleResize = () => {
|
||||
setViewportHeight(window.innerHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Set initial height
|
||||
setViewportHeight(window.innerHeight);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
const el = scrollRef.current;
|
||||
|
||||
if (!el) {
|
||||
@@ -82,25 +118,78 @@ export const useVirtualList = (options: VirtualListOptions): VirtualListResult =
|
||||
return () => observer.disconnect();
|
||||
}, [scrollRef]);
|
||||
|
||||
// Handle scroll events
|
||||
// Handle scroll events and calculate content offset
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (scrollRef === 'window') {
|
||||
const calculateOffset = () => {
|
||||
const contentEl = contentRef?.current;
|
||||
|
||||
if (!el) {
|
||||
if (!contentEl) {
|
||||
contentOffsetRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// For window scrolling, the offset is the distance from the top of the
|
||||
// content element to the top of the document, which is its bounding rect
|
||||
// top plus the current scroll position.
|
||||
contentOffsetRef.current = contentEl.getBoundingClientRect().top + window.scrollY;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
calculateOffset();
|
||||
|
||||
const adjustedScrollTop = Math.max(0, window.scrollY - contentOffsetRef.current);
|
||||
setScrollTop(adjustedScrollTop);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
// Set initial values
|
||||
calculateOffset();
|
||||
const adjustedScrollTop = Math.max(0, window.scrollY - contentOffsetRef.current);
|
||||
setScrollTop(adjustedScrollTop);
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
|
||||
const scrollEl = scrollRef.current;
|
||||
|
||||
if (!scrollEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
setScrollTop(el.scrollTop);
|
||||
const calculateOffset = () => {
|
||||
const contentEl = contentRef?.current;
|
||||
|
||||
if (!contentEl) {
|
||||
contentOffsetRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollRect = scrollEl.getBoundingClientRect();
|
||||
const contentRect = contentEl.getBoundingClientRect();
|
||||
|
||||
// The offset is the distance from the top of the content element to
|
||||
// the top of the scroll container, adjusted for current scroll position.
|
||||
contentOffsetRef.current = contentRect.top - scrollRect.top + scrollEl.scrollTop;
|
||||
};
|
||||
|
||||
el.addEventListener('scroll', handleScroll, { passive: true });
|
||||
const handleScroll = () => {
|
||||
calculateOffset();
|
||||
|
||||
// Set initial scroll position
|
||||
setScrollTop(el.scrollTop);
|
||||
const adjustedScrollTop = Math.max(0, scrollEl.scrollTop - contentOffsetRef.current);
|
||||
setScrollTop(adjustedScrollTop);
|
||||
};
|
||||
|
||||
return () => el.removeEventListener('scroll', handleScroll);
|
||||
}, [scrollRef]);
|
||||
scrollEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
// Set initial values
|
||||
calculateOffset();
|
||||
const adjustedScrollTop = Math.max(0, scrollEl.scrollTop - contentOffsetRef.current);
|
||||
setScrollTop(adjustedScrollTop);
|
||||
|
||||
return () => scrollEl.removeEventListener('scroll', handleScroll);
|
||||
}, [scrollRef, contentRef]);
|
||||
|
||||
// Get item size helper
|
||||
const getItemSize = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user