Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NXT-149 & NXT-151 Add IP notification and UK open content forms #113

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions web/src/components/JotFormEmbed/JotFormEmbed.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.iframe {
background: transparent;
border: 0;
max-width: 100%;
min-height: 540px;
min-width: 100%;
width: 100%;
}
177 changes: 177 additions & 0 deletions web/src/components/JotFormEmbed/JotFormEmbed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { render, screen, fireEvent } from "@testing-library/react";

import { JotFormEmbed } from "./JotFormEmbed";

describe("JotFormEmbed", () => {
it("should render iframe with title", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveProperty(
"tagName",
"IFRAME"
);
});

it("should create iframe source URL from form id, JotForm base URL and isIframeEmbed querystring", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveAttribute(
"src",
"https://next-web-tests.jotform.com/1234?isIframeEmbed=1"
);
});

it("should allow full screen on iframe", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveAttribute(
"allowfullscreen",
""
);
});

it("should allow geolocation, microphone and camera on iframe", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveAttribute(
"allow",
"geolocation; microphone; camera"
);
});

it("should use hidden overflow style", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveStyle({
overflow: "hidden",
});
});

it("should add data attribute with form ID for GTM tracking", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

expect(screen.getByTitle("This is a title")).toHaveAttribute(
"data-jotform-id",
"1234"
);
});

it("should use given initial height", () => {
render(
<JotFormEmbed jotFormID="1234" title="This is a title" height={999} />
);

expect(screen.getByTitle("This is a title")).toHaveStyle({
height: "999px",
});
});

it("should set height from iframe post message", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

fireEvent(
window,
new MessageEvent("message", {
data: "setHeight:987:1234",
origin: "https://next-web-tests.jotform.com",
})
);

expect(screen.getByTitle("This is a title")).toHaveStyle({
height: "987px",
});
});

it("should call given onSubmit callback prop after form submission event", () => {
const onSubmit = jest.fn();

render(
<JotFormEmbed
jotFormID="1234"
title="This is a title"
onSubmit={onSubmit}
/>
);

fireEvent(
window,
new MessageEvent("message", {
data: {
action: "submission-completed",
formID: "1234",
},
origin: "https://next-web-tests.jotform.com",
})
);

expect(onSubmit).toHaveBeenCalled();
});

it("should push submit event to data layer after form submission message", () => {
const dataLayerPush = jest.spyOn(window.dataLayer, "push");

render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

fireEvent(
window,
new MessageEvent("message", {
data: {
action: "submission-completed",
formID: "1234",
},
origin: "https://next-web-tests.jotform.com",
})
);

expect(dataLayerPush).toHaveBeenCalledWith({
event: "Jotform Message",
jf_id: "1234",
jf_title: "This is a title",
jf_type: "submit",
});

dataLayerPush.mockReset();
});

it("should push progress event to data layer after scroll into view message", () => {
const dataLayerPush = jest.spyOn(window.dataLayer, "push");

render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

fireEvent(
window,
new MessageEvent("message", {
data: "scrollIntoView::1234",
origin: "https://next-web-tests.jotform.com",
})
);

expect(dataLayerPush).toHaveBeenCalledWith({
event: "Jotform Message",
jf_id: "1234",
jf_title: "This is a title",
jf_type: "progress",
});

dataLayerPush.mockReset();
});

it("should scroll iframe into view in response to scrollIntoView message", () => {
render(<JotFormEmbed jotFormID="1234" title="This is a title" />);

const iframe = screen.getByTitle("This is a title"),
scrollIntoView = jest.fn();

iframe.scrollIntoView = scrollIntoView;

fireEvent(
window,
new MessageEvent("message", {
data: "scrollIntoView::1234",
origin: "https://next-web-tests.jotform.com",
})
);

expect(scrollIntoView).toHaveBeenCalled();
});
});
188 changes: 188 additions & 0 deletions web/src/components/JotFormEmbed/JotFormEmbed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React, {
CSSProperties,
FC,
useCallback,
useEffect,
useRef,
useState,
} from "react";

import { publicRuntimeConfig } from "@/config";
import type { FormID } from "@/feeds/jotform/jotform";
import { logger } from "@/logger";

import styles from "./JotFormEmbed.module.scss";

const jotFormBaseURL = publicRuntimeConfig.jotForm.baseURL;

interface JotFormEmbedProps {
jotFormID: FormID;
title: string;
/** An optional, initial height */
height?: number;
onSubmit?: () => void;
}

type JFMessageObject = {
action: "submission-completed";
formID: FormID;
};

type JFMessageName =
| "scrollIntoView"
| "setHeight"
| "setMinHeight"
| "collapseErrorPage"
| "reloadPage"
| "loadScript"
| "exitFullscreen";

type JFMessageString = `${JFMessageName}:${number | ""}:${FormID}`;

type JFMessageEvent = MessageEvent<JFMessageObject | JFMessageString>;

export const JotFormEmbed: FC<JotFormEmbedProps> = ({
jotFormID,
title,
height,
onSubmit,
}) => {
const iframeRef = useRef<HTMLIFrameElement>(null),
[styleOverrides, setStyleOverrides] = useState<CSSProperties>({
height: height ? `${height}px` : undefined,
}),
handleIFrameMessage = useCallback(
(content?: JFMessageEvent) => {
if (!iframeRef.current || !content || content.origin != jotFormBaseURL)
return;

const { data } = content;

// The form completion message is an object rather than a string like other messages so handle it first
if (
typeof data === "object" &&
data.action === "submission-completed"
) {
window.dataLayer.push({
event: "Jotform Message",
jf_type: "submit",
jf_id: jotFormID,
jf_title: title,
});
if (onSubmit) onSubmit();
return;
}

// Ignore non-string messages as they should all be strings in the format like "setHeight:1577:230793530776059"
if (typeof data !== "string") return;

const messageParts = data.split(":"),
[messageName, value, targetFormID] = messageParts,
iframe = iframeRef.current;

if (targetFormID !== jotFormID) {
logger.warn(
`Form with ID ${jotFormID} didn't match event with form ID ${targetFormID}`
);
return;
}

switch (messageName as JFMessageName) {
case "scrollIntoView":
if (typeof iframe.scrollIntoView === "function")
iframe.scrollIntoView();
// There's no 'page event' sent from JotForm for multi page forms,
// but scrollIntoView is fired for pages so we use this as the closest thing to track pagination
window.dataLayer.push({
event: "Jotform Message",
jf_type: "progress",
jf_id: jotFormID,
jf_title: title,
});
break;
case "setHeight": {
const height = parseInt(value, 10) + "px";
setStyleOverrides((s) => ({ ...s, height }));
break;
}
case "setMinHeight": {
const minHeight = parseInt(value, 10) + "px";
setStyleOverrides((s) => ({ ...s, minHeight }));
break;
}
case "reloadPage":
if (iframe.contentWindow) {
try {
iframe.contentWindow.location.reload();
} catch (e) {
window.location.reload();
}
} else window.location.reload();
break;
case "collapseErrorPage":
if (iframe.clientHeight > window.innerHeight) {
iframe.style.height = window.innerHeight + "px";
}
break;
case "exitFullscreen":
if (window.document.exitFullscreen)
window.document.exitFullscreen();
break;
case "loadScript": {
let src = value;
if (messageParts.length > 3) {
src = value + ":" + messageParts[2];
}

const script = document.createElement("script");
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
break;
}
default:
break;
}

if (iframe.contentWindow && iframe.contentWindow.postMessage) {
const urls = {
docurl: encodeURIComponent(global.document.URL),
referrer: encodeURIComponent(global.document.referrer),
};
iframe.contentWindow.postMessage(
JSON.stringify({ type: "urls", value: urls }),
"*"
);
}
},
[jotFormID, onSubmit, iframeRef, title]
);

useEffect(() => {
window.addEventListener("message", handleIFrameMessage, false);

return () =>
window.removeEventListener("message", handleIFrameMessage, false);
}, [handleIFrameMessage]);

useEffect(() => {
// Only hide the iframe scroll bar once JS has kicked in and we know we can respond to the setHeight message
setStyleOverrides((s) => ({ ...s, overflow: "hidden" }));
}, []);

return (
<iframe
id={`JotFormIFrame-${jotFormID}`}
data-jotform-id={jotFormID}
ref={iframeRef}
src={`${jotFormBaseURL}/${jotFormID}?isIframeEmbed=1`}
title={title}
allowFullScreen
allow="geolocation; microphone; camera"
className={styles.iframe}
style={styleOverrides}
//deprecated scrolling attr to prevent iframe scrollbar flickering on validation
// scrolling="no"
/>
);
};
Loading