feat(portal): add service drawers and employee portal preview
This commit is contained in:
39 files changed
+1446
-130
No files matched your search
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user