Skip to content

Contact modals on one native-dialog component, with consent-aware tracking - #773

Merged
vvlladd28 merged 2 commits into
mainfrom
form-guard
Oct 16, 2026
Merged

vvlladd28 merged 2 commits into
mainfrom
form-guard

Conversation

@vvlladd28

Copy link
Copy Markdown
Member

What

Shared form protection — @util/form-guard
Field validation with inline hints, the XSS pre-check, the honeypot, the render-to-submit timing check and consent-aware tracking fields, in one module. ContactForm, the dev-services form and the contact modals use it instead of their own copies.

One contact modal — Modal.astro + ContactModal.astro
Modal is a native <dialog> shell: title, close button, backdrop, the form-modal card, an intro slot. ContactModal puts the contact form in it. It replaces three separate modals:

Page Before Now
/services/development-services/ .dev-modal div, <img> close button not reachable by keyboard, no Esc, no aria-modal/name ContactModal
/products/trendz/ TrendzContactModal (deleted), hand-rolled focus trap, honeypot armed but never checked ContactModal
/smart-farming-demo/ painted under the header and chat launcher (inside the isolated .main-pane), alert() validation, no UTM/client_id/fpr/honeypot ContactModal, intro text via the slot, subject sent as a hidden field

All of them now:

  • sit in the top layer (above the header, promo banner and chat launcher);
  • trap focus, close on Esc and backdrop click, and return focus to the trigger;
  • post to FORMSPREE_CONTACT;
  • redirect to /contact-us-thanks/ (as ContactForm does);
  • fill utm_*/fpr only with Marketing consent and client_id only with Analytics consent, and refill on consent change.

Triggers use data-contact-modal-open (bare = the page's only contact modal). ServicesBanner's buttonOnClick prop becomes buttonOpensContactModal.

The dev-services in-page form gets client_id/fpr, FORMSPREE_CONTACT, and no longer validates twice per submit (inline onsubmit + listener).

Shared dialog wiring — @util/modal-dialog
bindModalDialog() handles the scroll lock, backdrop close, and unlocking after the exit fade. These now use it:

  • cookie preferences
  • Cloud region chooser
  • IoT Hub install dialog
  • blog image lightbox — now a <dialog>. Images open it from the keyboard too (Enter / Space), so each image in a post is now a Tab stop. Esc no longer releases another overlay's scroll lock.

Smaller fixes

  • Search overlay: role="dialog", aria-modal, aria-label, shared lockScroll. It stays a <div>, since Google CSE's suggestion dropdown is appended to <body> and would render under a top-layer dialog.
  • IoT Hub install dialog: aria-labelledby.
  • PromoBanner.astro: z-index comment had the cookie banner at 9999; it is 1100.
  • Removed the now-unused public/images/close-icon.svg.

Visual changes

  • dev-services modal: the close icon is a 20px stroke X (was a 28px image); fields stack to one column below 750px (was 960px).
  • smart-farming modal: new look, the same as the dev-services and Trendz modals; the intro text keeps its two lines (16px lead, 14px muted note).
  • Trendz modal: site typography mixins instead of trendz-*, otherwise the same layout.

Behaviour notes

  • The timing check now counts from opening a modal, not from page load.
  • Trendz and smart-farming modal forms now carry gtm_form/gtm_button, as the dev-services one already did.
  • smart-farming no longer pre-selects a subject from ?subject=; it always sends Custom Development.

Checked

Manually in Chrome on the dev server (no form was submitted; validation and bot paths were exercised with synthetic submit events):

  • every dialog above: open/close, focus, Esc, backdrop, scroll lock;
  • consent: with, without Marketing, without any consent, restored;
  • dark theme.

pnpm lint:eslint passes on the touched files. Not run yet: pnpm check, pnpm build, pnpm lint:linkcheck.

Follow-ups

  • Poc in two weeks #760's Services/ContactModal.astro can be replaced by <ContactModal id="poc-contact" title="Let's talk about your project" formId="poc_sales_modal" tags="sales" />; its bare data-contact-modal-open triggers work unchanged. Until then it still references the removed close-icon.svg.
  • The /pricing/ calculator modals still use portalToBody + their own controller.

…cking

- Add `@util/form-guard`: field validation with inline hints, XSS pre-check,
  honeypot, timing check and consent-aware tracking fields, shared by
  ContactForm, the dev-services form and the contact modals.
- Add `Modal.astro` (native <dialog> shell) and `ContactModal.astro` on top of
  it; dev-services, Trendz and smart-farming now use it. The old modals
  rendered inside the isolated main pane, so the header and the chat launcher
  painted over them; the smart-farming one also sent no tracking fields and
  validated with alert().
- Contact modals redirect to /contact-us-thanks/ and send client_id and fpr;
  the dev-services form uses FORMSPREE_CONTACT and validates once per submit.
- Add `@util/modal-dialog` (scroll lock, backdrop close, unlock after the exit
  fade) and move the cookie preferences, Cloud region, IoT Hub install and
  blog image lightbox dialogs onto it. The lightbox is now a <dialog> that
  opens from the keyboard.
- Search overlay: role=dialog, aria-modal, a label and the shared scroll lock.
- IoT Hub install dialog gets an accessible name; fix the stale z-index
  comment in PromoBanner; remove the unused close-icon.svg.

@vvlladd28 vvlladd28 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Reviewed 20 changed files in Contact modals on one native-dialog component, with consent-aware tracking. Left 1 comment inline.

The one finding is in the shared backdrop-close check in src/util/modal-dialog.ts: it holds for the cookie, Cloud region and IoT Hub dialogs, whose inner panel fills the dialog box, but not for Modal.astro, where the <dialog> is the padded card itself. Clicks on the padding, or a text selection dragged out of a field, close the contact modal and drop what the visitor typed.

Not reviewed: public/images/close-icon.svg (deleted asset) — no reviewable logic.


This review was auto-generated. Findings may contain errors — please verify before applying changes.

Comment thread src/util/modal-dialog.ts Outdated
The <dialog> was the padded card itself, so a click on its padding, or a
text selection dragged out of a field, landed on the dialog and closed the
contact modal. The card moves into an inner panel inside a transparent,
viewport-filling dialog, and the backdrop close also requires the press to
start on the backdrop.
@vvlladd28
vvlladd28 merged commit 2d9e59c into main Oct 16, 2026
4 checks passed
@vvlladd28 vvlladd28 mentioned this pull request Oct 19, 2026
7 of 15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant