import { ALERT_DISMISS_EVENT, type AlertDismissDetail, type AlertOptions, type AlertVariant, } from './alert.types'; const SELECTORS = { dismiss: '[data-c42-alert-dismiss]', } as const; const VARIANTS: readonly AlertVariant[] = ['info', 'success', 'warning', 'error']; const ASSERTIVE: ReadonlySet = new Set(['warning', 'error']); /** * Headless alert/callout controller. Sets the appropriate ARIA live semantics * for the variant, reflects `data-variant` / `data-state`, and (when a * `[data-c42-alert-dismiss]` element is present) wires dismissal. It never * applies visual styles — variant colors live in CSS keyed off `data-variant`. * * Markup: * ```html *
* *
* Heads up *

Your trial ends soon.

*
* *
* ``` */ export class Alert { private readonly root: HTMLElement; private readonly dismissBtn: HTMLElement | null; private variant: AlertVariant; private readonly roleOverride: 'alert' | 'status' | undefined; private dismissed = false; private cleanups: Array<() => void> = []; constructor(root: HTMLElement, options: AlertOptions = {}) { this.root = root; this.roleOverride = options.role; this.variant = this.normalizeVariant(options.variant ?? root.dataset.variant); this.dismissBtn = root.querySelector(SELECTORS.dismiss); this.applyVariant(); this.root.dataset.state = 'open'; if (this.dismissBtn) { if ( this.dismissBtn instanceof HTMLButtonElement && !this.dismissBtn.hasAttribute('type') ) { this.dismissBtn.type = 'button'; } const onClick = (): void => this.dismiss(); this.dismissBtn.addEventListener('click', onClick); this.cleanups.push(() => this.dismissBtn?.removeEventListener('click', onClick)); } } private normalizeVariant(value: string | undefined): AlertVariant { return VARIANTS.includes(value as AlertVariant) ? (value as AlertVariant) : 'info'; } private applyVariant(): void { this.root.dataset.variant = this.variant; const role = this.roleOverride ?? (ASSERTIVE.has(this.variant) ? 'alert' : 'status'); this.root.setAttribute('role', role); this.root.setAttribute('aria-live', role === 'alert' ? 'assertive' : 'polite'); } /** Change the variant; updates `data-variant` and ARIA live semantics. */ setVariant(variant: AlertVariant): void { const next = this.normalizeVariant(variant); if (next === this.variant) { return; } this.variant = next; this.applyVariant(); } get currentVariant(): AlertVariant { return this.variant; } /** Hide the alert and emit `alert:dismiss`. No-op if already dismissed. */ dismiss(): void { if (this.dismissed) { return; } this.dismissed = true; this.root.dataset.state = 'dismissed'; this.root.setAttribute('hidden', ''); const detail: AlertDismissDetail = { variant: this.variant }; this.root.dispatchEvent(new CustomEvent(ALERT_DISMISS_EVENT, { detail, bubbles: true })); } /** Re-show a dismissed alert. */ show(): void { if (!this.dismissed) { return; } this.dismissed = false; this.root.dataset.state = 'open'; this.root.removeAttribute('hidden'); } /** Subscribe to a DOM event on the root element. Returns an unsubscribe fn. */ on(event: string, handler: (event: E) => void): () => void { const listener = handler as EventListener; this.root.addEventListener(event, listener); const off = (): void => this.root.removeEventListener(event, listener); this.cleanups.push(off); return off; } destroy(): void { this.cleanups.forEach((fn) => fn()); this.cleanups = []; } }