UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

307 lines (306 loc) 11.3 kB
/** * ARIA Utilities for Therapeutic Components * * Comprehensive ARIA attribute management specifically designed for therapeutic * and ADHD-focused components with enhanced semantic meaning and context. */ /** * Generate complete ARIA attributes for therapeutic components */ export function buildTherapeuticAriaProps(props) { const ariaProps = {}; // Basic ARIA attributes if (props.label) ariaProps['aria-label'] = props.label; if (props.labelledBy) ariaProps['aria-labelledby'] = props.labelledBy; if (props.describedBy) ariaProps['aria-describedby'] = props.describedBy; if (props.role) ariaProps.role = props.role; // State attributes if (props.current) ariaProps['aria-current'] = props.current; if (props.expanded !== undefined) ariaProps['aria-expanded'] = props.expanded; if (props.pressed !== undefined) ariaProps['aria-pressed'] = props.pressed; if (props.checked !== undefined) ariaProps['aria-checked'] = props.checked; if (props.selected !== undefined) ariaProps['aria-selected'] = props.selected; if (props.disabled !== undefined) ariaProps['aria-disabled'] = props.disabled; if (props.hidden !== undefined) ariaProps['aria-hidden'] = props.hidden; // Progress attributes if (props.progressValue !== undefined) ariaProps['aria-valuenow'] = props.progressValue; if (props.progressMax !== undefined) ariaProps['aria-valuemax'] = props.progressMax; if (props.progressText) ariaProps['aria-valuetext'] = props.progressText; // Live region attributes if (props.liveRegion) ariaProps['aria-live'] = props.liveRegion; if (props.atomic !== undefined) ariaProps['aria-atomic'] = props.atomic; if (props.relevant) ariaProps['aria-relevant'] = props.relevant; // Therapeutic-specific data attributes for styling and behavior if (props.therapeuticType) ariaProps['data-therapeutic-type'] = props.therapeuticType; if (props.emotionalState) ariaProps['data-emotional-state'] = props.emotionalState; if (props.supportLevel) ariaProps['data-support-level'] = props.supportLevel; if (props.cognitiveLoad) ariaProps['data-cognitive-load'] = props.cognitiveLoad; if (props.interactionMode) ariaProps['data-interaction-mode'] = props.interactionMode; if (props.focusSupport) ariaProps['data-focus-support'] = props.focusSupport; if (props.helpAvailable !== undefined) ariaProps['data-help-available'] = props.helpAvailable; if (props.completionState) ariaProps['data-completion-state'] = props.completionState; // Custom properties if (props.customProps) { Object.entries(props.customProps).forEach(([key, value]) => { ariaProps[`data-${key}`] = value; }); } return ariaProps; } /** * Create ARIA description for mood tracking components */ export function createMoodAriaDescription(mood) { const parts = [ `Mood tracker: current level ${mood.value} out of ${mood.scale === '1-5' ? 5 : mood.scale === '1-10' ? 10 : 5}.`, `Mood state: ${mood.label}. ${mood.description}.`, `Input method: ${mood.inputMethod}.` ]; if (mood.history && mood.history > 0) { parts.push(`${mood.history} previous entries recorded.`); } parts.push('Use arrow keys to adjust, Enter to confirm, h for help.'); return parts.join(' '); } /** * Create ARIA description for energy level indicators */ export function createEnergyAriaDescription(energy) { const parts = [ `Energy level indicator: ${energy.level} at ${energy.value}%.`, energy.description, `Displayed as ${energy.variant} visualization.` ]; if (energy.interactive) { parts.push('Interactive mode: use arrow keys to change level, Enter to select.'); } parts.push('Press h for help and guidance.'); return parts.join(' '); } /** * Create ARIA description for breathing exercises */ export function createBreathingAriaDescription(breathing) { if (!breathing.active) { return `Breathing exercise: not started. Pattern: ${breathing.pattern.join('-')} seconds. Press Enter or Space to begin, h for help.`; } const parts = [ `Breathing exercise active: ${breathing.phase} phase.`, `Cycle ${breathing.cycleCount} completed.` ]; if (breathing.paused) { parts.push('Exercise paused. Press Space to resume, Escape to stop.'); } else { parts.push('Follow the visual guide. Press Space to pause, Escape to stop.'); } return parts.join(' '); } /** * Create ARIA description for crisis mode controls */ export function createCrisisModeAriaDescription(crisis) { const parts = []; if (crisis.enabled && crisis.severity) { parts.push(`Crisis mode active at ${crisis.severity} level.`); parts.push('Interface simplified to reduce cognitive load.'); if (crisis.adaptations && crisis.adaptations.length > 0) { parts.push(`Active adaptations: ${crisis.adaptations.join(', ')}.`); } parts.push('Press Escape for immediate deactivation.'); } else { parts.push('Crisis mode controls: not active.'); parts.push('Choose severity level or use quick activation.'); } if (crisis.autoDetect) { parts.push('Automatic detection enabled.'); } parts.push('Crisis mode provides simplified interface during stress. Safe to use anytime.'); return parts.join(' '); } /** * Create live region for therapeutic announcements */ export function createTherapeuticLiveRegion(config) { const liveRegion = document.createElement('div'); liveRegion.id = config.id; liveRegion.setAttribute('aria-live', config.priority); liveRegion.setAttribute('aria-atomic', String(config.atomic ?? true)); if (config.relevant) { liveRegion.setAttribute('aria-relevant', config.relevant); } liveRegion.className = `sr-only therapeutic-live-region ${config.className || ''}`.trim(); return liveRegion; } /** * Update therapeutic live region with debouncing */ export function updateTherapeuticLiveRegion(regionId, message, options = {}) { const region = document.getElementById(regionId); if (!region) return; const { debounce = 1000, clear = true, delay = 0 } = options; // Clear existing timeout const timeoutKey = `therapeutic-live-${regionId}`; const existingTimeout = window[timeoutKey]; if (existingTimeout) { clearTimeout(existingTimeout); } // Set new timeout window[timeoutKey] = setTimeout(() => { region.textContent = message; // Auto-clear after reading if (clear) { setTimeout(() => { region.textContent = ''; }, Math.max(message.length * 50, 3000)); // Estimate reading time } }, delay); } /** * Create therapeutic component landmark */ export function createTherapeuticLandmark(config) { const attrs = { role: config.type, 'aria-label': config.label }; if (config.description) { attrs['aria-describedby'] = `${config.label.toLowerCase().replace(/\s+/g, '-')}-description`; } if (config.therapeuticContext) { attrs['data-therapeutic-context'] = config.therapeuticContext; } return attrs; } /** * Generate keyboard shortcut announcements for therapeutic components */ export function announceKeyboardShortcuts(shortcuts) { const shortcutList = Object.entries(shortcuts.shortcuts) .map(([key, action]) => `${key.replace(/\+/g, ' plus ')} for ${action}`) .join(', '); let announcement = `${shortcuts.component} keyboard shortcuts: ${shortcutList}.`; if (shortcuts.context) { announcement += ` ${shortcuts.context}`; } announcement += ' Press h anytime for help.'; return announcement; } /** * Create accessible error messages for therapeutic components */ export function createTherapeuticErrorMessage(error) { const parts = [error.message]; if (error.suggestion) { parts.push(error.suggestion); } if (error.recovery_action) { parts.push(`To recover: ${error.recovery_action}`); } if (error.emotional_support !== false) { switch (error.type) { case 'validation': parts.push('Take your time. All input is valuable.'); break; case 'user-input': parts.push('No judgment here. Try again when ready.'); break; case 'timeout': parts.push('Sometimes a break is exactly what we need.'); break; default: parts.push('Technical issues happen. Your progress matters.'); } } return parts.join(' '); } /** * Build comprehensive screen reader content for complex therapeutic states */ export function buildScreenReaderContent(state) { const parts = []; // Component and current state parts.push(`${state.component} current state:`); // Key state information (limit to most important) const stateEntries = Object.entries(state.currentState) .slice(0, 3) // Limit to prevent overwhelming .map(([key, value]) => `${key}: ${value}`) .join(', '); if (stateEntries) { parts.push(stateEntries); } // Progress information if (state.progressInfo) { parts.push(`Progress: ${state.progressInfo.current} of ${state.progressInfo.total}. ${state.progressInfo.description}`); } // Available actions if (state.availableActions.length > 0) { parts.push(`Available actions: ${state.availableActions.slice(0, 3).join(', ')}`); } // Help hints if (state.helpHints.length > 0) { parts.push(`Help: ${state.helpHints.slice(0, 2).join('. ')}`); } // Emotional context if (state.emotionalContext) { parts.push(state.emotionalContext); } return parts.join('. ') + '.'; } /** * Export utility for creating hidden descriptions */ export function createHiddenDescription(id, content) { const element = document.createElement('div'); element.id = id; element.className = 'sr-only therapeutic-hidden-description'; element.textContent = content; return element; } /** * Validate ARIA therapeutic props for development */ export function validateTherapeuticAriaProps(props) { const warnings = []; // Check for essential therapeutic properties if (!props.therapeuticType) { warnings.push('therapeuticType should be specified for therapeutic components'); } if (!props.label && !props.labelledBy) { warnings.push('Either label or labelledBy should be provided for accessibility'); } if (props.progressValue !== undefined && props.progressMax === undefined) { warnings.push('progressMax should be provided when progressValue is set'); } if (props.helpAvailable === undefined) { warnings.push('helpAvailable should be explicitly set for therapeutic components'); } return warnings; }