/** * Shared state for the registration dialog. * * `` is mounted exactly once (app.vue), so every call to action on * the site — header, hero, courier band, registration section — drives the same instance. * Consequence worth keeping: the form keeps whatever the visitor already typed when the * dialog is closed and reopened. * * …which is exactly why the dialog used to reopen on the "check your inbox" screen after * a successful submission: `done` and the form data are per-instance state, and closing * the dialog never touched them. `openDialog()` therefore bumps `resetToken`, and * RegistrationForm listens for it. The reset happens on OPEN, never on close, so * dismissing the dialog halfway through typing still preserves the input. * * The element that opened the dialog is held in a module-level variable rather than in * `useState`, because it is a DOM node (never serialisable into the SSR payload). It is * only ever written from a browser event handler. */ export type RegistrationPreset = '' | 'klinik' | 'kurierdienst' let lastTrigger: HTMLElement | null = null export const useRegistrationDialog = () => { const open = useState('b24-registration-dialog', () => false) const presetType = useState('b24-registration-preset', () => '') /** Incremented on every open; RegistrationForm watches it to clear finished state. */ const resetToken = useState('b24-registration-reset', () => 0) /** * Opens the dialog. Pass the click event so focus can be returned to the exact CTA that * opened it; pass a preset to skip step 1 for CTAs that already state the type * ("Als Kurierdienst registrieren"). */ const openDialog = (event?: Event | null, preset: RegistrationPreset = '') => { if (import.meta.client) { const candidate = (event?.currentTarget ?? document.activeElement) as HTMLElement | null lastTrigger = candidate && typeof candidate.focus === 'function' ? candidate : null } // Assigned unconditionally: a preset left over from an earlier CTA must not leak into // an open that deliberately asks for no preselection ("Registrierung starten"). presetType.value = preset resetToken.value++ open.value = true } const closeDialog = () => { open.value = false } /** Returns focus to the CTA that opened the dialog (WCAG 2.4.3 "Focus Order"). */ const restoreTriggerFocus = () => { const el = lastTrigger lastTrigger = null if (!import.meta.client || !el || !document.contains(el)) return // One frame later: the dialog is out of the top layer by then, so no scroll fight. requestAnimationFrame(() => el.focus()) } return { open, presetType, resetToken, openDialog, closeDialog, restoreTriggerFocus } }