diff --git a/components/MarketoContactForm.tsx b/components/MarketoContactForm.tsx index 8c3705f7df..8dbcb7dedf 100644 --- a/components/MarketoContactForm.tsx +++ b/components/MarketoContactForm.tsx @@ -31,6 +31,23 @@ declare global { } } +function applyRequiredAsterisks(formElement: HTMLElement) { + if (typeof formElement.querySelectorAll !== "function") { + return; + } + + formElement + .querySelectorAll(".mktoAsterix") + .forEach((asterisk) => { + const isRequired = Boolean(asterisk.closest(".mktoRequiredField")); + asterisk.style.setProperty( + "display", + isRequired ? "inline" : "none", + "important", + ); + }); +} + function MarketoSuccessPanel() { return (
@@ -71,6 +88,11 @@ export function MarketoContactForm() { MARKETO_MUNCHKIN_ID, MARKETO_FORM_ID, (form) => { + const renderedForm = document.getElementById(MARKETO_FORM_ELEMENT_ID); + if (renderedForm) { + applyRequiredAsterisks(renderedForm); + } + setIsFormLoaded(true); form.onSuccess(() => { diff --git a/scripts/test-use-case-analytics.mjs b/scripts/test-use-case-analytics.mjs index 7a6a724459..e5f56f9199 100644 --- a/scripts/test-use-case-analytics.mjs +++ b/scripts/test-use-case-analytics.mjs @@ -469,7 +469,42 @@ test("sales completion is emitted only after Marketo success and is deduplicated }, }, }; - globalThis.document = { getElementById: () => ({ replaceChildren() {} }) }; + const requiredAsterisk = { + display: null, + priority: null, + closest: (selector) => + selector === ".mktoRequiredField" ? { required: true } : null, + style: { + setProperty(name, value, priority) { + if (name === "display") { + requiredAsterisk.display = value; + requiredAsterisk.priority = priority; + } + }, + }, + }; + const optionalAsterisk = { + display: null, + priority: null, + closest: () => null, + style: { + setProperty(name, value, priority) { + if (name === "display") { + optionalAsterisk.display = value; + optionalAsterisk.priority = priority; + } + }, + }, + }; + globalThis.document = { + getElementById: () => ({ + replaceChildren() {}, + querySelectorAll(selector) { + assert.equal(selector, ".mktoAsterix"); + return [requiredAsterisk, optionalAsterisk]; + }, + }), + }; const form = load(compile("components/MarketoContactForm.tsx"), (name) => { if (name === "react") return { @@ -496,6 +531,10 @@ test("sales completion is emitted only after Marketo success and is deduplicated }); try { form.MarketoContactForm(); + assert.equal(requiredAsterisk.display, "inline"); + assert.equal(requiredAsterisk.priority, "important"); + assert.equal(optionalAsterisk.display, "none"); + assert.equal(optionalAsterisk.priority, "important"); assert.equal(captured.length, 0, "Loading a form is not a conversion"); assert.equal(onSuccess({ Email: "test@example.invalid" }, ""), false); assert.equal(onSuccess({}, ""), false); @@ -511,3 +550,53 @@ test("sales completion is emitted only after Marketo success and is deduplicated delete globalThis.document; } }); + +test("sales completion still registers when the form stub has no querySelectorAll", () => { + const captured = []; + let onSuccess; + globalThis.window = { + MktoForms2: { + loadForm(_base, _id, _form, callback) { + callback({ + onSuccess: (fn) => { + onSuccess = fn; + }, + }); + }, + }, + }; + globalThis.document = { getElementById: () => ({ replaceChildren() {} }) }; + const form = load(compile("components/MarketoContactForm.tsx"), (name) => { + if (name === "react") + return { + useState: (value) => [value, () => {}], + useRef: (value) => ({ current: value }), + useCallback: (fn) => fn, + useEffect: (fn) => fn(), + }; + if (name === "react/jsx-runtime") + return { + jsx: (type, props) => ({ type, props }), + jsxs: (type, props) => ({ type, props }), + }; + if (name === "lucide-react") return { Check: () => null }; + if (name === "posthog-js") + return { + capture: (event, props) => captured.push({ event, props }), + }; + if (name === "@/lib/ad-conversions") + return { reportTalkToUsConversion: () => {} }; + if (name === "@/lib/use-case-analytics") + return { readUseCaseAttribution: () => attribution }; + throw Error(name); + }); + try { + assert.doesNotThrow(() => form.MarketoContactForm()); + assert.equal(typeof onSuccess, "function"); + assert.equal(onSuccess({}, ""), false); + assert.equal(captured[0]?.event, "sales:inquiry_completed"); + } finally { + delete globalThis.window; + delete globalThis.document; + } +}); diff --git a/style.css b/style.css index 9e3553a71b..0de5eae135 100644 --- a/style.css +++ b/style.css @@ -978,8 +978,17 @@ line-height: 1.25rem !important; } -.lf-marketo-form .mktoAsterix { - display: inline !important; +/* Marketo always emits .mktoAsterix in every label, then hides it unless + the field wrap has .mktoRequiredField (from the form descriptor's + IsRequired: true). Do not force-show every asterisk — that marks + optional fields such as Phone as required. + + Selectors must beat the Marketo ThemeStyleOverride + `.lf-marketo-form .mktoAsterix { display: contents !important }` which + is injected after page CSS. The form id is always present; `.mktoForm` + is not reliably on the
when Forms2 finishes loading. */ +.lf-marketo-form form[id^="mktoForm_"] .mktoAsterix { + display: none !important; float: none !important; line-height: inherit !important; padding-left: 0 !important; @@ -987,6 +996,16 @@ color: hsl(var(--destructive)) !important; } +.lf-marketo-form form[id^="mktoForm_"] .mktoRequiredField .mktoAsterix { + display: inline !important; +} + +/* ThemeStyleOverride also injects " *" via ::after on required labels. + Suppress it so required fields do not show two marks. */ +.lf-marketo-form form[id^="mktoForm_"] .mktoRequiredField .mktoLabel::after { + content: none !important; +} + .lf-marketo-form input[type="text"], .lf-marketo-form input[type="email"], .lf-marketo-form input[type="tel"],