All files / src/pages/AcquirerPortal/LegalDocuments/LegalDocumentDetail/components DocumentEditor.tsx

86.36% Statements 19/22
75% Branches 9/12
83.33% Functions 5/6
90% Lines 18/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                      10x     10x   10x 33x                               120x 33x 33x 33x                   120x 120x 120x   120x 29x 29x 29x       29x 29x     120x                                                                                                                    
import { useEffect, useMemo, useRef, useState } from "react";
import { Box } from "@mui/material";
import {
  ContentState,
  EditorState,
  RawDraftContentState,
  convertFromRaw,
} from "draft-js";
import HFRichInput from "@shared/HFInputs/HFRichInput/HFRichInput";
 
// Legal documents are long-form; lift the editor's default short-input cap.
const DOCUMENT_MAX_LENGTH = 200000;
 
// Gap between the sticky banner and the floating toolbar (matches the mockup).
const TOOLBAR_TOP_GAP = 20;
 
const isRawDraftContent = (value: unknown): value is RawDraftContentState =>
  !!value &&
  typeof value === "object" &&
  Array.isArray((value as RawDraftContentState).blocks);
 
interface DocumentEditorProps {
  content: unknown;
  onChange: (state: EditorState) => void;
  /** Height of the sticky page banner; the toolbar floats just beneath it. */
  stickyTop?: number;
}
 
export default function DocumentEditor({
  content,
  onChange,
  stickyTop = 0,
}: DocumentEditorProps) {
  const initialContent = useMemo<ContentState>(() => {
    try {
      Eif (isRawDraftContent(content)) {
        return convertFromRaw(content);
      }
    } catch {
      // fall through to an empty document
    }
    return ContentState.createFromText("");
  }, [content]);
 
  // The toolbar is sticky; raise a shadow only once it actually sticks (i.e.
  // the user has scrolled the content underneath it), per the mockup.
  const sentinelRef = useRef<HTMLDivElement>(null);
  const [isStuck, setIsStuck] = useState(false);
  const toolbarOffset = stickyTop + TOOLBAR_TOP_GAP;
 
  useEffect(() => {
    const el = sentinelRef.current;
    Iif (!el || typeof IntersectionObserver === "undefined") return;
    const observer = new IntersectionObserver(
      ([entry]) => setIsStuck(!entry.isIntersecting),
      { rootMargin: `-${toolbarOffset}px 0px 0px 0px`, threshold: 0 },
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [toolbarOffset]);
 
  return (
    <Box>
      {/* Marks where the toolbar sits unstuck; once it scrolls past the sticky
          offset the toolbar gains a shadow. */}
      <Box ref={sentinelRef} sx={{ height: 0 }} />
      <Box
        sx={{
          "& .toolbarClassName": {
            position: "sticky",
            top: `${toolbarOffset}px`,
            zIndex: 10,
            // Center the toolbar items on a common baseline — without this the
            // shorter icon buttons sit higher than the taller "Paragraph"
            // dropdown (they aren't vertically centered against each other).
            display: "flex",
            alignItems: "center",
            width: "fit-content",
            maxWidth: "100%",
            marginLeft: "auto",
            marginRight: "auto",
            marginTop: `${TOOLBAR_TOP_GAP}px`,
            marginBottom: "20px",
            boxShadow: isStuck ? "0 4px 16px rgba(0, 0, 0, 0.12)" : "none",
            transition: "box-shadow 0.2s ease",
          },
          // The document variant omits the form-input box entirely (border,
          // 172px height, padding) — only the page-specific minimum height is
          // declared here, so nothing races HFRichInput's Container styles in
          // the cascade. An empty draft still presents a page-like surface.
          "& .editorClassName": {
            minHeight: "60vh",
          },
        }}
      >
        <HFRichInput
          value={initialContent}
          onChange={onChange}
          options={[
            "headings",
            "paragraph-small",
            "alignments",
            "ordered-items",
            // Fee-schedule tables (Merchant Agreement) — see TableBlock.tsx
            "table",
            // Keep a pasted Word/Google Docs agreement's headings, emphasis,
            // alignment, lists and links instead of flattening them to plain
            // text — see pastedContent.ts.
            "rich-paste",
          ]}
          maxLength={DOCUMENT_MAX_LENGTH}
          label={null}
          showCharCount={false}
          variant="document"
        />
      </Box>
    </Box>
  );
}