mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
fix: improve embed mobile experience
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
@ -83,12 +83,15 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(false);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(false);
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [hasAutoExpanded, setHasAutoExpanded] = useState(false);
|
||||||
|
|
||||||
const [isEmailLocked, setIsEmailLocked] = useState(false);
|
const [isEmailLocked, setIsEmailLocked] = useState(false);
|
||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
|
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const documentEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||||
|
|
||||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
||||||
@ -317,6 +320,43 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
|
// Set up intersection observer to auto-expand widget when user reaches bottom of document
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a delay to ensure document has fully rendered and stabilized
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Get the number of pages in the document
|
||||||
|
const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length;
|
||||||
|
|
||||||
|
// Only set up the observer if there's more than one page
|
||||||
|
if (pageCount <= 1) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries;
|
||||||
|
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
setHasAutoExpanded(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (documentEndRef.current) {
|
||||||
|
observer.observe(documentEndRef.current);
|
||||||
|
}
|
||||||
|
}, 1500); // 1.5 second delay
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]);
|
||||||
|
|
||||||
if (hasCompletedDocument) {
|
if (hasCompletedDocument) {
|
||||||
return (
|
return (
|
||||||
<EmbedDocumentCompleted
|
<EmbedDocumentCompleted
|
||||||
@ -344,6 +384,8 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
/>
|
/>
|
||||||
|
{/* Observer target at the bottom of the document */}
|
||||||
|
<div ref={documentEndRef} className="h-4 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
@ -360,19 +402,34 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<LucideChevronDown
|
<Button
|
||||||
className="text-muted-foreground h-5 w-5"
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
>
|
||||||
) : (
|
<LucideChevronDown className="text-muted-foreground h-5 w-5" />
|
||||||
<LucideChevronUp
|
|
||||||
className="text-muted-foreground h-5 w-5"
|
|
||||||
onClick={() => setIsExpanded(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
) : pendingFields.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0 md:hidden"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<LucideChevronUp className="text-muted-foreground h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="md:hidden"
|
||||||
|
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => throttledOnCompleteClick()}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
@ -91,11 +91,14 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [hasAutoExpanded, setHasAutoExpanded] = useState(false);
|
||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
|
|
||||||
|
const documentEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
@ -241,6 +244,42 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
|
// Set up intersection observer to auto-expand widget when user reaches bottom of document
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a delay to ensure document has fully rendered and stabilized
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length;
|
||||||
|
|
||||||
|
// Only set up the observer if there's more than one page
|
||||||
|
if (pageCount <= 1) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const [entry] = entries;
|
||||||
|
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
setHasAutoExpanded(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (documentEndRef.current) {
|
||||||
|
observer.observe(documentEndRef.current);
|
||||||
|
}
|
||||||
|
}, 1500); // 1.5 second delay
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]);
|
||||||
|
|
||||||
if (hasRejectedDocument) {
|
if (hasRejectedDocument) {
|
||||||
return <EmbedDocumentRejected name={fullName} />;
|
return <EmbedDocumentRejected name={fullName} />;
|
||||||
}
|
}
|
||||||
@ -283,6 +322,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
/>
|
/>
|
||||||
|
{/* Observer target at the bottom of the document */}
|
||||||
|
<div ref={documentEndRef} className="h-4 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
@ -303,19 +344,36 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<LucideChevronDown
|
<Button
|
||||||
className="text-muted-foreground h-5 w-5"
|
variant="outline"
|
||||||
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
onClick={() => setIsExpanded(false)}
|
onClick={() => setIsExpanded(false)}
|
||||||
/>
|
>
|
||||||
) : (
|
<LucideChevronDown className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
<LucideChevronUp
|
|
||||||
className="text-muted-foreground h-5 w-5"
|
|
||||||
onClick={() => setIsExpanded(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
) : pendingFields.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-background dark:bg-foreground h-8 w-8 p-0 md:hidden"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<LucideChevronUp className="text-muted-foreground dark:text-background h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="md:hidden"
|
||||||
|
disabled={
|
||||||
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
|
}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => throttledOnCompleteClick()}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user