feat(forms): add service inquiry drawers

This commit is contained in:
DeCentN2Madness committed 2026-08-18 18:49:59 -07:00
1 parent ac43ae3669
commit cfd6e630a3
4 files changed
+523

No files matched your search

+3
View File
@@ -7,6 +7,9 @@
- Language and direction attributes for English, Spanish, and Arabic. - Language and direction attributes for English, Spanish, and Arabic.
- Native `details` elements for mobile navigation and FAQ accordions. - Native `details` elements for mobile navigation and FAQ accordions.
- Form labels, autocomplete hints, disabled submission state, and explanatory text. - Form labels, autocomplete hints, disabled submission state, and explanatory text.
- Service inquiry drawers use native modal dialogs with focus containment,
Escape, backdrop, and labeled close-button dismissal plus reduced-motion
support.
- Reduced decorative image announcements through empty alt text where appropriate. - Reduced decorative image announcements through empty alt text where appropriate.
- Responsive typography and layouts without fixed text sizing. - Responsive typography and layouts without fixed text sizing.
- Language-switcher choices are limited to routes that actually exist. - Language-switcher choices are limited to routes that actually exist.
+7
View File
@@ -80,6 +80,13 @@ backend and spam-protection TODO comments. Submission is intentionally
disabled. Appointment and enrollment calls to action route to the static disabled. Appointment and enrollment calls to action route to the static
consultation page rather than retaining the production Jotform backend. consultation page rather than retaining the production Jotform backend.
The three English service pages also reproduce the live site's delayed,
right-edge inquiry drawers, including their page-specific enrollment copy,
shared field set, modal overlay, and responsive layout. These drawer forms are
static and their submit controls remain disabled until a production backend
and spam protection are selected; this is an intentional functional and visual
deviation from the live HubSpot form.
## External Dependencies ## External Dependencies
- The contact page map intentionally matches the live Leaflet/OpenStreetMap - The contact page map intentionally matches the live Leaflet/OpenStreetMap
@@ -0,0 +1,511 @@
---
import ContactObfuscation from './ContactObfuscation.astro';
type ServiceKey = 'aba-therapy' | 'autism-evaluations' | 'learner-social-club';
interface Props {
service: ServiceKey;
}
const serviceCopy: Record<ServiceKey, {
heading: string[];
serviceName: string;
statusLead: string;
status: string;
prompt: string;
}> = {
'aba-therapy': {
heading: ['No WAITLIST!'],
serviceName: 'ABA Therapy',
statusLead: 'is',
status: 'currently accepting new patients',
prompt: "To help guarantee your child's admission to our program, please provide their information."
},
'autism-evaluations': {
heading: ['Enrollment', 'Now Open'],
serviceName: 'Autism Evaluations',
statusLead: 'are',
status: 'now open for enrollment',
prompt: "To help guarantee your child's admission into one of our upcoming programs, please provide their information."
},
'learner-social-club': {
heading: ['Enrollment', 'Now Open'],
serviceName: 'Learner Social Club',
statusLead: 'is',
status: 'now open for enrollment',
prompt: "To help guarantee your child's spot in one of our upcoming group enrichment programs, please provide their information."
}
};
const { service } = Astro.props;
const copy = serviceCopy[service];
const drawerId = `service-inquiry-${service}`;
const titleId = `${drawerId}-title`;
const noteId = `${drawerId}-note`;
---
<dialog
class="service-inquiry-drawer"
data-service-inquiry-drawer
data-dismiss-key={`aia-service-inquiry-dismissed:${service}`}
aria-labelledby={titleId}
>
<button class="service-inquiry-drawer__close" type="button" data-service-inquiry-close aria-label="Close inquiry form">
<svg aria-hidden="true" viewBox="0 0 32 32" width="18" height="18">
<path d="M4 4 28 28M28 4 4 28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="3" />
</svg>
</button>
<header class="service-inquiry-drawer__intro">
<h2 id={titleId} data-service-inquiry-title tabindex="-1">
{copy.heading.map((line, index) => <Fragment>{index > 0 && <br />}{line}</Fragment>)}
</h2>
<p>
Arizona Institute for Autism's<br />
<strong>{copy.serviceName}</strong><br />
{copy.statusLead} <em>{copy.status}</em>.
</p>
<p>{copy.prompt}</p>
</header>
<form class="service-inquiry-form" method="post" action="">
<!-- TODO: Wire this form to Netlify Forms, a custom API endpoint, or another backend service. -->
<!-- TODO: Add spam protection before production launch. -->
<div class="service-inquiry-form__grid">
<label>
<span>First Name<span class="required-mark" aria-hidden="true">*</span></span>
<input name="first_name" autocomplete="given-name" required />
</label>
<label>
<span>Last Name<span class="required-mark" aria-hidden="true">*</span></span>
<input name="last_name" autocomplete="family-name" required />
</label>
<label>
<span>Phone Number<span class="required-mark" aria-hidden="true">*</span></span>
<span class="service-inquiry-form__phone">
<span class="service-inquiry-form__country">
<span aria-hidden="true">🇺🇸</span>
<span class="service-inquiry-form__country-caret" aria-hidden="true"></span>
<span class="visually-hidden">United States country code</span>
</span>
<span class="service-inquiry-form__phone-number">
<span aria-hidden="true">+1</span>
<input type="tel" name="phone" autocomplete="tel-national" required />
</span>
</span>
</label>
<label>
<span>Email<span class="required-mark" aria-hidden="true">*</span></span>
<input type="email" name="email" autocomplete="email" required />
</label>
<label>
<span>Child First Name</span>
<input name="child_first_name" autocomplete="off" />
</label>
<label>
<span>Child Last Name</span>
<input name="child_last_name" autocomplete="off" />
</label>
<label class="service-inquiry-form__wide">
<span>Child Birth Date</span>
<input name="child_birth_date" inputmode="numeric" autocomplete="bday" placeholder="MM - DD - YYYY" />
</label>
<label>
<span>Location Interest</span>
<select name="location_interest">
<option value="" selected>Please Select</option>
<option value="aia_scottsdale">AIA Scottsdale</option>
<option value="aia_glendale">AIA Glendale</option>
<option value="other_none">Other/None</option>
</select>
</label>
<label>
<span>Service Interest</span>
<select name="service_interest">
<option value="" selected>Please Select</option>
<option value="behavioral">Behavioral - ABA Therapy Service</option>
<option value="psychological">Psychological - Autism Evaluation Service</option>
<option value="sociological">Sociological - Learners Social Club Service</option>
<option value="other_none">Other/None</option>
</select>
</label>
<label>
<span>Insurance &amp; Payment Options</span>
<select name="payment_options">
<option value="" selected>Please select</option>
<option>United Healthcare AHCCCS</option>
<option>BCBS AZ</option>
<option>United Healthcare</option>
<option>Optum</option>
<option>Tricare</option>
<option>Care Credit</option>
<option>Private Pay</option>
<option value="Other">Other (please describe in comment)</option>
</select>
</label>
<label>
<span>How did you hear about us?</span>
<select name="referred_by">
<option value="" selected>Please select</option>
<option>Facebook</option>
<option>Google</option>
<option>Primary Doctor</option>
<option value="Specialist">Specialist (e.g., Developmental Psychologist)</option>
<option>Client Referral</option>
<option>Community Event</option>
<option value="Other">Other (please describe in comment)</option>
</select>
</label>
<label class="service-inquiry-form__wide">
<span>Additional Information/Comments</span>
<textarea name="comment" rows="2"></textarea>
</label>
</div>
<div class="service-inquiry-form__footer">
<p id={noteId}>Online submission is not yet connected. Please call <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</p>
<button type="submit" disabled aria-describedby={noteId}>Submit</button>
</div>
</form>
</dialog>
<script>
const initializeServiceInquiryDrawers = () => {
document.querySelectorAll('[data-service-inquiry-drawer]').forEach((element) => {
if (!(element instanceof HTMLDialogElement) || element.dataset.initialized === 'true') return;
element.dataset.initialized = 'true';
const closeButton = element.querySelector('[data-service-inquiry-close]');
const title = element.querySelector('[data-service-inquiry-title]');
const dismissKey = element.dataset.dismissKey;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let closeTimer: number | undefined;
const wasDismissed = () => {
try {
return dismissKey ? sessionStorage.getItem(dismissKey) === 'true' : false;
} catch {
return false;
}
};
const rememberDismissal = () => {
try {
if (dismissKey) sessionStorage.setItem(dismissKey, 'true');
} catch {
// Storage can be unavailable in privacy-focused browsing modes.
}
};
const finishClose = () => {
if (element.open) element.close();
document.documentElement.classList.remove('service-inquiry-open');
};
const closeDrawer = () => {
if (!element.open) return;
rememberDismissal();
element.classList.remove('is-open');
window.clearTimeout(closeTimer);
if (reduceMotion.matches) {
finishClose();
return;
}
closeTimer = window.setTimeout(finishClose, 500);
};
const openDrawer = () => {
if (element.open || wasDismissed()) return;
element.showModal();
document.documentElement.classList.add('service-inquiry-open');
requestAnimationFrame(() => requestAnimationFrame(() => {
element.classList.add('is-open');
if (title instanceof HTMLElement) title.focus({ preventScroll: true });
}));
};
closeButton?.addEventListener('click', closeDrawer);
element.addEventListener('cancel', (event) => {
event.preventDefault();
closeDrawer();
});
element.addEventListener('click', (event) => {
if (event.target === element) closeDrawer();
});
element.addEventListener('close', () => {
element.classList.remove('is-open');
document.documentElement.classList.remove('service-inquiry-open');
});
window.setTimeout(openDrawer, 10_000);
});
};
initializeServiceInquiryDrawers();
</script>
<style>
:global(html.service-inquiry-open) {
overflow: hidden;
}
.service-inquiry-drawer {
background: white;
border: 0;
border-radius: 4px;
box-sizing: border-box;
color: var(--color-primary);
font-family: var(--font-body);
inset: 10px 10px auto auto;
margin: 0;
max-height: calc(100dvh - 20px);
max-width: calc(100vw - 20px);
opacity: 0;
overflow-y: auto;
padding: 0;
transform: translateX(calc(100% + 20px));
transition: opacity 300ms ease, transform 500ms ease;
width: 608px;
}
.service-inquiry-drawer.is-open {
opacity: 1;
transform: translateX(0);
}
.service-inquiry-drawer::backdrop {
background: rgb(0 0 0 / 56%);
opacity: 0;
transition: opacity 300ms ease;
}
.service-inquiry-drawer.is-open::backdrop {
opacity: 1;
}
.service-inquiry-drawer__close {
align-items: center;
background: transparent;
border: 0;
color: black;
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
position: absolute;
right: 10px;
top: 10px;
z-index: 1;
}
.service-inquiry-drawer__close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.service-inquiry-drawer__intro {
padding: 24px 60px 0;
text-align: center;
}
.service-inquiry-drawer__intro h2 {
font-family: var(--font-body);
font-size: 1.625rem;
line-height: 1.05;
margin: 10px 0 16px;
outline: none;
}
.service-inquiry-drawer__intro p {
line-height: 1.2;
margin: 0 0 16px;
}
.service-inquiry-drawer__intro p:last-child {
margin-bottom: 0;
}
.service-inquiry-form {
padding: 48px 60px 60px;
}
.service-inquiry-form__grid {
display: grid;
gap: 18px 30px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.service-inquiry-form label {
display: grid;
font-weight: 400;
gap: 7px;
line-height: 1.2;
min-width: 0;
}
.required-mark {
color: #e51520;
}
.service-inquiry-form input,
.service-inquiry-form select,
.service-inquiry-form textarea {
background: #f5f8fa;
border: 1px solid #959494;
border-radius: 3px;
box-sizing: border-box;
color: #21303d;
font: inherit;
min-height: 41px;
padding: 10px;
width: 100%;
}
.service-inquiry-form input::placeholder,
.service-inquiry-form textarea::placeholder {
color: rgb(81 99 131 / 64%);
}
.service-inquiry-form select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath fill='%2321303d' d='M0 0h10L5 6z'/%3E%3C/svg%3E");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 10px 6px;
color: rgb(81 99 131 / 70%);
padding-right: 34px;
}
.service-inquiry-form textarea {
min-height: 58px;
resize: vertical;
}
.service-inquiry-form__wide {
grid-column: 1 / -1;
}
.service-inquiry-form__phone {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
}
.service-inquiry-form__phone input {
background: transparent;
border: 0;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
min-width: 0;
padding-left: 6px;
}
.service-inquiry-form__country {
align-items: center;
background: #f5f8fa;
border: 1px solid #959494;
border-radius: 3px 0 0 3px;
color: #21303d;
display: flex;
gap: 7px;
padding: 0 10px;
}
.service-inquiry-form__country-caret {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #21303d;
height: 0;
width: 0;
}
.service-inquiry-form__phone-number {
align-items: center;
background: #f5f8fa;
border: 1px solid #959494;
border-left: 0;
border-radius: 0 3px 3px 0;
color: #21303d;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
padding-left: 10px;
}
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.service-inquiry-form__footer {
align-items: end;
display: grid;
gap: 20px;
grid-template-columns: minmax(0, 1fr) auto;
margin-top: 40px;
}
.service-inquiry-form__footer p {
color: var(--color-primary);
font-size: .8rem;
line-height: 1.35;
margin: 0;
}
.service-inquiry-form__footer button {
background: var(--color-accent-strong);
border: 0;
border-radius: 3px;
color: white;
font: inherit;
font-weight: 700;
opacity: 1;
padding: 12px 24px;
}
.service-inquiry-form__footer button:disabled {
cursor: not-allowed;
}
@media (max-width: 500px) {
.service-inquiry-drawer__intro {
padding-top: 20px;
}
.service-inquiry-form__grid {
grid-template-columns: 1fr;
}
.service-inquiry-form__wide {
grid-column: auto;
}
.service-inquiry-form__footer {
align-items: stretch;
grid-template-columns: 1fr;
}
.service-inquiry-form__footer button {
justify-self: end;
}
}
@media (max-width: 360px) {
.service-inquiry-drawer__intro,
.service-inquiry-form {
padding-inline: 32px;
}
}
@media (prefers-reduced-motion: reduce) {
.service-inquiry-drawer,
.service-inquiry-drawer::backdrop {
transition: none;
}
}
</style>
+2
View File
@@ -8,6 +8,7 @@ import CareersPage from '../components/pages/CareersPage.astro';
import ContactPage from '../components/pages/ContactPage.astro'; import ContactPage from '../components/pages/ContactPage.astro';
import FaqPage from '../components/pages/FaqPage.astro'; import FaqPage from '../components/pages/FaqPage.astro';
import PageHero from '../components/PageHero.astro'; import PageHero from '../components/PageHero.astro';
import ServiceInquiryDrawer from '../components/ServiceInquiryDrawer.astro';
import { pageHeroImages } from '../data/page-visuals'; import { pageHeroImages } from '../data/page-visuals';
const { entry } = Astro.props; const { entry } = Astro.props;
const showForm = ['client-consultation', 'schedule-consultation', 'referrals'].includes(entry.data.slug); const showForm = ['client-consultation', 'schedule-consultation', 'referrals'].includes(entry.data.slug);
@@ -69,5 +70,6 @@ const heroImage = pageHeroImages[entry.data.slug];
: entry.data.slug === 'contact' : entry.data.slug === 'contact'
? <ContactPage title={bannerTitle}><slot /></ContactPage> ? <ContactPage title={bannerTitle}><slot /></ContactPage>
: <article class:list={['prose', 'source-page', 'container', { 'service-page': isService }]}><slot /></article>} : <article class:list={['prose', 'source-page', 'container', { 'service-page': isService }]}><slot /></article>}
{isService && entry.data.lang === 'en' && <ServiceInquiryDrawer service={entry.data.slug} />}
{showForm && <FormShell title={bannerTitle} showBackground={entry.data.slug === 'client-consultation'} />} {showForm && <FormShell title={bannerTitle} showBackground={entry.data.slug === 'client-consultation'} />}
</BaseLayout> </BaseLayout>