mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 05:43:12 +00:00
fix(comp-doc): keep the page pill current on re-layout and stamp NOT REQUIRED on the sheet
The page being read is re-derived after every render and every observed resize, not only when the page count changes. The NOT REQUIRED stamp is rendered inside the first sheet, above its greyed-out content, instead of floating over the scroll panel.
This commit is contained in:
parent
dc2565e3fe
commit
727588ab52
3 changed files with 144 additions and 92 deletions
|
|
@ -15,7 +15,7 @@ type CompDocDocumentFrameProps = {
|
|||
sections: readonly ReactNode[];
|
||||
/** Greyed-out pages, as for a document marked not required. */
|
||||
dimmed?: boolean;
|
||||
/** Rendered over the preview, outside the dimmed layer. */
|
||||
/** Stamped over the first sheet, outside the dimmed layer. */
|
||||
overlay?: ReactNode;
|
||||
/** Page being read and page count, updated on layout and on scroll. */
|
||||
onPageChange?: (position: PagePosition) => void;
|
||||
|
|
@ -39,85 +39,83 @@ export function CompDocDocumentFrame({
|
|||
useCompDocPagination(sections.length, onPageChange);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
style={{
|
||||
opacity: dimmed ? 0.5 : 1,
|
||||
filter: dimmed ? "grayscale(0.4)" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
onScroll={onScroll}
|
||||
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto"
|
||||
style={{ padding: "0 20px 20px", scrollbarGutter: "stable" }}
|
||||
data-testid="comp-doc-document"
|
||||
>
|
||||
<div ref={sheetsRef} className="relative">
|
||||
{pages.map((blockIndices, pageIndex) => (
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
onScroll={onScroll}
|
||||
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto"
|
||||
style={{ padding: "0 20px 20px", scrollbarGutter: "stable" }}
|
||||
data-testid="comp-doc-document"
|
||||
>
|
||||
<div ref={sheetsRef} className="relative">
|
||||
{pages.map((blockIndices, pageIndex) => (
|
||||
<div
|
||||
key={pageIndex}
|
||||
ref={(el) => {
|
||||
pageRefs.current[pageIndex] = el;
|
||||
}}
|
||||
data-testid="comp-doc-page"
|
||||
className="relative flex flex-col break-words"
|
||||
style={{
|
||||
marginTop: pageIndex > 0 ? PAGE_GAP_PX : 0,
|
||||
minHeight: pageHeight,
|
||||
background: "#ffffff",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,.08), 0 12px 32px -16px rgba(0,0,0,.25)",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-sans)",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex flex-1 flex-col"
|
||||
style={{
|
||||
opacity: dimmed ? 0.5 : 1,
|
||||
filter: dimmed ? "grayscale(0.4)" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
key={pageIndex}
|
||||
ref={(el) => {
|
||||
pageRefs.current[pageIndex] = el;
|
||||
}}
|
||||
data-testid="comp-doc-page"
|
||||
className="relative flex flex-col break-words"
|
||||
data-testid="comp-doc-page-top-bar"
|
||||
style={{
|
||||
marginTop: pageIndex > 0 ? PAGE_GAP_PX : 0,
|
||||
minHeight: pageHeight,
|
||||
background: "#ffffff",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,.08), 0 12px 32px -16px rgba(0,0,0,.25)",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-sans)",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
height: PAGE_TOP_BAR_PX,
|
||||
flexShrink: 0,
|
||||
background: "linear-gradient(100deg,#27AAE1,#1C75BC)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid="comp-doc-page-top-bar"
|
||||
style={{
|
||||
height: PAGE_TOP_BAR_PX,
|
||||
flexShrink: 0,
|
||||
background: "linear-gradient(100deg,#27AAE1,#1C75BC)",
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: `0 28px ${PAGE_BOTTOM_PADDING_PX}px` }}>
|
||||
{pageIndex === 0 && (
|
||||
<div ref={headerRef} style={flowRoot}>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
{blockIndices.map((blockIndex) => (
|
||||
<div
|
||||
key={blockIndex}
|
||||
ref={(el) => {
|
||||
blockRefs.current[blockIndex] = el;
|
||||
}}
|
||||
style={flowRoot}
|
||||
>
|
||||
{sections[blockIndex]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
data-testid="comp-doc-page-bottom-bar"
|
||||
style={{
|
||||
height: PAGE_BOTTOM_BAR_PX,
|
||||
flexShrink: 0,
|
||||
marginTop: "auto",
|
||||
background: "linear-gradient(100deg,#1C75BC,#27AAE1)",
|
||||
clipPath: "polygon(0 45%,100% 0,100% 100%,0 100%)",
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
<div style={{ padding: `0 28px ${PAGE_BOTTOM_PADDING_PX}px` }}>
|
||||
{pageIndex === 0 && (
|
||||
<div ref={headerRef} style={flowRoot}>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
{blockIndices.map((blockIndex) => (
|
||||
<div
|
||||
key={blockIndex}
|
||||
ref={(el) => {
|
||||
blockRefs.current[blockIndex] = el;
|
||||
}}
|
||||
style={flowRoot}
|
||||
>
|
||||
{sections[blockIndex]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="comp-doc-page-bottom-bar"
|
||||
style={{
|
||||
height: PAGE_BOTTOM_BAR_PX,
|
||||
flexShrink: 0,
|
||||
marginTop: "auto",
|
||||
background: "linear-gradient(100deg,#1C75BC,#27AAE1)",
|
||||
clipPath: "polygon(0 45%,100% 0,100% 100%,0 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{overlay}
|
||||
{pageIndex === 0 && overlay}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,23 +63,16 @@ export function useCompDocPagination(
|
|||
measure();
|
||||
});
|
||||
|
||||
// Width changes, late font/image loads and reflow do not re-render; observe them.
|
||||
const layoutKey = pages.map((page) => page.join(",")).join("|");
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => measure());
|
||||
[sheetsRef.current, headerRef.current, ...blockRefs.current.slice(0, blockCount)].forEach(
|
||||
(el) => el && observer.observe(el),
|
||||
);
|
||||
return () => observer.disconnect();
|
||||
}, [measure, blockCount, layoutKey]);
|
||||
|
||||
const onPageChangeRef = useRef(onPageChange);
|
||||
const totalRef = useRef(pages.length);
|
||||
const lastReported = useRef<PagePosition | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
onPageChangeRef.current = onPageChange;
|
||||
totalRef.current = pages.length;
|
||||
});
|
||||
|
||||
const report = useCallback((total: number) => {
|
||||
const report = useCallback(() => {
|
||||
const total = totalRef.current;
|
||||
const tops = pageRefs.current.slice(0, total).map((el) => el?.offsetTop ?? 0);
|
||||
const current = Math.min(currentPageAt(tops, scrollerRef.current?.scrollTop ?? 0), total);
|
||||
const last = lastReported.current;
|
||||
|
|
@ -88,12 +81,23 @@ export function useCompDocPagination(
|
|||
onPageChangeRef.current?.({ current, total });
|
||||
}, []);
|
||||
|
||||
const total = pages.length;
|
||||
// Any re-layout can move the sheet tops under the reader, even when the page count holds.
|
||||
useLayoutEffect(() => {
|
||||
report(total);
|
||||
}, [report, total]);
|
||||
report();
|
||||
});
|
||||
|
||||
const onScroll = useCallback(() => report(total), [report, total]);
|
||||
// Width changes, late font/image loads and reflow do not re-render; observe them.
|
||||
const layoutKey = pages.map((page) => page.join(",")).join("|");
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
measure();
|
||||
report();
|
||||
});
|
||||
[sheetsRef.current, headerRef.current, ...blockRefs.current.slice(0, blockCount)].forEach(
|
||||
(el) => el && observer.observe(el),
|
||||
);
|
||||
return () => observer.disconnect();
|
||||
}, [measure, report, blockCount, layoutKey]);
|
||||
|
||||
return {
|
||||
pages,
|
||||
|
|
@ -103,6 +107,6 @@ export function useCompDocPagination(
|
|||
headerRef,
|
||||
blockRefs,
|
||||
pageRefs,
|
||||
onScroll,
|
||||
onScroll: report,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const SHEET_WIDTH = 420;
|
|||
const LETTERHEAD_HEIGHT = 200;
|
||||
const BLOCK_HEIGHT = 300;
|
||||
const SHEET_STRIDE = 614;
|
||||
let sheetStride = SHEET_STRIDE;
|
||||
|
||||
function isLetterhead(el: HTMLElement) {
|
||||
return el.querySelector('img[alt="Seahaven Industries"]') !== null;
|
||||
|
|
@ -33,12 +34,13 @@ function stubRect(this: HTMLElement): DOMRect {
|
|||
const offsetTopDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetTop");
|
||||
|
||||
beforeEach(() => {
|
||||
sheetStride = SHEET_STRIDE;
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(stubRect);
|
||||
Object.defineProperty(HTMLElement.prototype, "offsetTop", {
|
||||
configurable: true,
|
||||
get(this: HTMLElement) {
|
||||
if (this.dataset.testid !== "comp-doc-page") return 0;
|
||||
return Array.from(this.parentElement?.children ?? []).indexOf(this) * SHEET_STRIDE;
|
||||
return Array.from(this.parentElement?.children ?? []).indexOf(this) * sheetStride;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -155,6 +157,33 @@ describe("Completion template preview pagination", () => {
|
|||
expect(within(sheets()[2]).getByText("Step group 3")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Live preview/)).toHaveTextContent("Live preview · Page 1 of 3");
|
||||
});
|
||||
|
||||
it("re-reads the page being read when an edit moves the sheets but keeps the page count", () => {
|
||||
const { rerender } = render(
|
||||
<CompletionTemplatePreview
|
||||
values={{ name: "Edits", extraSafetyNote: null, procedures }}
|
||||
isEditing
|
||||
/>,
|
||||
);
|
||||
scrollDocumentTo(SHEET_STRIDE + 5);
|
||||
expect(screen.getByText(/Live preview/)).toHaveTextContent("Live preview · Page 2 of 3");
|
||||
|
||||
// The edit makes sheet 1 taller: sheet 2 now starts below the reader's position.
|
||||
sheetStride = 800;
|
||||
rerender(
|
||||
<CompletionTemplatePreview
|
||||
values={{
|
||||
name: "Edits",
|
||||
extraSafetyNote: null,
|
||||
procedures: [{ ...procedures[0], description: "Longer text" }, ...procedures.slice(1)],
|
||||
}}
|
||||
isEditing
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(sheets()).toHaveLength(3);
|
||||
expect(screen.getByText(/Live preview/)).toHaveTextContent("Live preview · Page 1 of 3");
|
||||
});
|
||||
});
|
||||
|
||||
const site: CompDocSiteInfo = {
|
||||
|
|
@ -206,6 +235,27 @@ describe("Work order completion document preview pagination", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("stamps NOT REQUIRED on the first sheet, above the greyed-out content", () => {
|
||||
render(
|
||||
<CompDocPreview
|
||||
row={row}
|
||||
site={site}
|
||||
notes="Replaced the filter on unit 3."
|
||||
generatedAt=""
|
||||
isGenerated={false}
|
||||
isNotRequired
|
||||
/>,
|
||||
);
|
||||
|
||||
const [first, second] = sheets();
|
||||
const stamp = within(first).getByText(/NOT REQUIRED/);
|
||||
expect(stamp.closest('[style*="opacity: 0.5"]')).toBeNull();
|
||||
expect(within(first).getByTestId("comp-doc-page-top-bar").parentElement).toHaveStyle({
|
||||
opacity: "0.5",
|
||||
});
|
||||
expect(within(second).queryByText(/NOT REQUIRED/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps a document without comments on a single sheet", () => {
|
||||
render(
|
||||
<CompDocPreview
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue