feat(portal): add service drawers and employee portal preview

This commit is contained in:
DeCentN2Madness committed 2026-10-30 00:03:21 -07:00
1 parent ff80ee516f
commit e97f0ea9b4
39 files changed
+1446 -130

No files matched your search

+3 -1
View File
@@ -1,6 +1,7 @@
---
interface Props {
href?: string;
serviceInquiry?: string;
variant?: 'filled' | 'outlined' | 'light';
size?: 'default' | 'small';
center?: boolean;
@@ -14,6 +15,7 @@ interface Props {
const {
href,
serviceInquiry,
variant = 'filled',
size = 'default',
center = false,
@@ -34,7 +36,7 @@ const classes = [
---
{href ? (
<a class:list={classes} href={href} target={target} rel={rel}><slot /></a>
<a class:list={classes} href={href} data-service-inquiry-trigger={serviceInquiry} target={target} rel={rel}><slot /></a>
) : (
<button class:list={classes} type={type} disabled={disabled} aria-describedby={ariaDescribedby}><slot /></button>
)}
+13 -4
View File
@@ -88,13 +88,20 @@ const headingId = `${idBase}-heading`;
if (timer) window.clearTimeout(timer);
// A late frame load supersedes the transport timeout. It is still not
// treated as a success, conversion, or cross-origin health signal.
container.dataset.jotformDocumentLoaded = 'true';
hideFallback();
};
const dispose = () => {
const dispose = (event) => {
disposed = true;
if (timer) window.clearTimeout(timer);
frame.removeEventListener('load', onLoad);
container.removeEventListener('aia:jotform-activate', activate);
window.removeEventListener('pagehide', dispose);
document.removeEventListener('astro:before-swap', dispose);
delete container.dataset.jotformFallbackInitialized;
if (event?.type === 'pagehide' && event.persisted) {
window.addEventListener('pageshow', () => { if (container.isConnected) initialize(container); }, { once: true });
}
};
const activate = () => {
@@ -103,9 +110,11 @@ const headingId = `${idBase}-heading`;
const approvedUrl = frame.dataset.jotformIframeUrl;
if (deferred && !approvedUrl) return;
activated = true;
frame.addEventListener('load', onLoad, { once: true });
timer = window.setTimeout(showFallback, fallbackTimeoutMs);
if (deferred) frame.src = approvedUrl;
if (container.dataset.jotformDocumentLoaded !== 'true') {
frame.addEventListener('load', onLoad, { once: true });
timer = window.setTimeout(showFallback, fallbackTimeoutMs);
}
if (deferred && frame.getAttribute('src') !== approvedUrl) frame.src = approvedUrl;
container.dataset.jotformIframeActivated = 'true';
};
+10 -103
View File
@@ -8,7 +8,7 @@ import {
resolveMockFormResult,
} from '../utils/form-integration';
type ServiceKey = 'aba-therapy' | 'autism-evaluations' | 'learner-social-club';
import { serviceInquiryCopy, type ServiceKey } from '../data/service-inquiry';
type SupportedLocale = 'en' | 'es';
interface Props {
@@ -22,11 +22,13 @@ const serviceNames: Record<SupportedLocale, Record<ServiceKey, string>> = {
'aba-therapy': 'ABA Therapy',
'autism-evaluations': 'Autism Evaluations',
'learner-social-club': 'Learner Social Club',
'aia-preparatory-academy': 'AIA Preparatory Academy',
},
es: {
'aba-therapy': 'Terapia ABA',
'autism-evaluations': 'Evaluaciones de autismo',
'learner-social-club': 'Learner Social Club',
'aia-preparatory-academy': 'AIA Preparatory Academy',
},
};
@@ -116,6 +118,7 @@ const drawerCopy: Record<SupportedLocale, {
const { service, lang, route } = Astro.props;
const copy = drawerCopy[lang];
const serviceName = serviceNames[lang][service];
const serviceCopy = serviceInquiryCopy[service][lang];
const drawerId = `service-inquiry-${lang}-${service}`;
const titleId = `${drawerId}-title`;
const noteId = `${drawerId}-note`;
@@ -137,15 +140,6 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
---
{consultationTestEnabled && formAIframe.url && <>
<button
class="service-inquiry-drawer__open"
type="button"
data-service-inquiry-open
aria-controls={drawerId}
aria-haspopup="dialog"
>
{copy.open(serviceName)}
</button>
<noscript>
<p class="service-inquiry-drawer__noscript">
{copy.noScript} <a href={formAIframe.url} target="_blank" rel="noopener noreferrer">{copy.hostedLink}</a>
@@ -157,8 +151,8 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
id={drawerId}
class="service-inquiry-drawer"
data-service-inquiry-drawer
data-explicit-activation={consultationTestEnabled ? 'true' : undefined}
data-dismiss-key={consultationTestEnabled ? undefined : `aia-service-inquiry-dismissed:${lang}:${service}`}
data-consultation-test={consultationTestEnabled ? 'true' : undefined}
data-service={service}
aria-labelledby={titleId}
>
<button class="service-inquiry-drawer__close" type="button" data-service-inquiry-close aria-label={copy.close}>
@@ -168,14 +162,14 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
</button>
<header class="service-inquiry-drawer__intro">
<h2 id={titleId} data-service-inquiry-title tabindex="-1">{consultationTestEnabled ? copy.heading(serviceName) : serviceName}</h2>
<p>{consultationTestEnabled ? copy.introduction : copy.legacyStatus}</p>
<h2 id={titleId} data-service-inquiry-title tabindex="-1">{consultationTestEnabled ? serviceCopy.heading : serviceName}</h2>
<p>{consultationTestEnabled ? serviceCopy.introduction : copy.legacyStatus}</p>
{consultationTestEnabled && <p class="service-inquiry-drawer__privacy">{copy.privacy}</p>}
</header>
{consultationTestEnabled
? <div class="service-inquiry-drawer__iframe">
<JotformIframe config={formAIframe} heading={copy.heading(serviceName)} variant="drawer" deferUntilActivated hideHeading />
<JotformIframe config={formAIframe} heading={serviceCopy.heading} variant="drawer" deferUntilActivated hideHeading />
</div>
: <div class="service-inquiry-form" role="group" data-aia-form data-form-mode={mode} data-form-mode-reason={config.reason} data-logical-form="service" data-mock-result={mockResult} data-mock-success="Demo only: the local fixture completed without sending anything." data-mock-failed="Demo only: the local fixture simulated a failed submission." data-mock-unavailable="Demo only: the local fixture simulated an unavailable service.">
<div class="service-inquiry-form__grid">
@@ -278,95 +272,8 @@ const consultationTestEnabled = formAIframe.enabled && formAIframe.context === '
<script>
import { bindMockForms } from '../scripts/mock-form';
import { initializeServiceInquiryDrawers } from '../scripts/service-inquiry';
bindMockForms();
const initializeServiceInquiryDrawers = () => {
document.querySelectorAll('[data-service-inquiry-drawer]').forEach((element) => {
if (!(element instanceof HTMLDialogElement) || element.dataset.initialized === 'true') return;
element.dataset.initialized = 'true';
const explicitActivation = element.dataset.explicitActivation === 'true';
const closeButton = element.querySelector('[data-service-inquiry-close]');
const title = element.querySelector('[data-service-inquiry-title]');
const frameAdapter = element.querySelector('[data-jotform-form-a]');
const triggers = Array.from(document.querySelectorAll(`[data-service-inquiry-open][aria-controls="${element.id}"]`));
const dismissKey = element.dataset.dismissKey;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let closeTimer: number | undefined;
let trigger: HTMLElement | 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;
if (!explicitActivation) rememberDismissal();
element.classList.remove('is-open');
window.clearTimeout(closeTimer);
if (reduceMotion.matches) {
finishClose();
return;
}
closeTimer = window.setTimeout(finishClose, 500);
};
const openDrawer = (openedBy?: EventTarget | null) => {
if (element.open || (!explicitActivation && wasDismissed())) return;
trigger = openedBy instanceof HTMLElement ? openedBy : undefined;
element.showModal();
document.documentElement.classList.add('service-inquiry-open');
if (explicitActivation && frameAdapter instanceof HTMLElement) {
frameAdapter.dispatchEvent(new Event('aia:jotform-activate'));
}
requestAnimationFrame(() => requestAnimationFrame(() => {
element.classList.add('is-open');
if (closeButton instanceof HTMLElement) closeButton.focus({ preventScroll: true });
else if (title instanceof HTMLElement) title.focus({ preventScroll: true });
}));
};
closeButton?.addEventListener('click', closeDrawer);
triggers.forEach((openButton) => openButton.addEventListener('click', () => openDrawer(openButton)));
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');
if (trigger instanceof HTMLElement) {
trigger.focus({ preventScroll: true });
trigger = undefined;
}
});
if (!explicitActivation) window.setTimeout(() => openDrawer(undefined), 10_000);
});
};
initializeServiceInquiryDrawers();
</script>
@@ -0,0 +1,42 @@
---
import { readPortalInput } from '../../utils/portal-input.mjs';
import Button from '../Button.astro';
// Non-PUBLIC, build-only ciphertext input. No passphrase is read by the build.
const payload = readPortalInput({
approved: import.meta.env.AIA_PORTAL_LOCAL_PREVIEW_APPROVED,
indexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
environment: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
file: import.meta.env.AIA_PORTAL_CIPHERTEXT_FILE,
});
const serialized = payload ? JSON.stringify(payload).replaceAll('<', '\\u003c') : 'null';
---
<section class="employee-portal container" data-employee-portal>
<div data-portal-prompt>
<h2>Access Code Required</h2>
<p>Please enter the appropriate access code to view your Arizona Institute for Autism Employee Portal.</p>
<!-- Local unlock only: no native form, action, server submission or persistence. -->
<div class="employee-portal__access" role="group" aria-label="Employee portal access">
<label for="employee-portal-code">Access Code</label>
<input id="employee-portal-code" type="password" autocomplete="off" disabled aria-describedby="employee-portal-feedback" data-portal-code />
<Button type="button" disabled>Unlock</Button>
</div>
<p id="employee-portal-feedback" role="status" aria-live="polite" data-portal-feedback>Portal access is unavailable until JavaScript and approved encrypted content are available.</p>
<noscript><p>JavaScript is required to unlock the employee portal locally. Please contact AIA for access assistance.</p></noscript>
</div>
<div class="employee-portal__content" tabindex="-1" role="region" aria-label="Unlocked employee portal" hidden data-portal-content></div>
<script is:inline type="application/json" data-portal-payload set:html={serialized}></script>
</section>
<script>import '../../scripts/employee-portal';</script>
<style>
.employee-portal { padding-block: 3rem; overflow-wrap: anywhere; }
.employee-portal__access { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
input { background: #fcf9f5; border: 1px solid #edeef3; border-radius: 4px; font: inherit; padding: .65rem; max-width: 100%; min-width: 0; }
input:focus-visible, .employee-portal__content:focus-visible { outline: 3px solid var(--color-focus, #115bbd); outline-offset: 3px; }
[hidden] { display: none; }
.employee-portal__content :global(a) { color: var(--color-primary); text-decoration: underline; }
.employee-portal__content :global(a:focus-visible) { outline: 3px solid var(--color-focus, #115bbd); outline-offset: 3px; }
.employee-portal__content :global(table) { display: block; overflow-x: auto; max-width: 100%; }
.employee-portal__content :global(td), .employee-portal__content :global(th) { padding: .5rem; }
@media (max-width: 500px) { .employee-portal__access { align-items: stretch; flex-direction: column; } }
</style>