UNPKG

claritykit-svelte

Version:

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

356 lines (350 loc) 11.8 kB
/** * ADHD-Specific Focus Management Utilities * * Provides specialized focus management, attention support, and interaction tracking * optimized for ADHD and neurodivergent users in therapeutic components. */ // Global focus tracking state let currentFocusSession = null; let focusHistory = []; let attentionMetrics = new Map(); let announcements = new Map(); // component -> last announcement time // Default configuration const defaultConfig = { enableFocusTracking: true, enableAnnouncementDebouncing: true, enableHyperfocusWarning: true, enableInteractionPattern: true, debounceDelay: 1000, hyperfocusThreshold: 10 * 60 * 1000, // 10 minutes hesitationThreshold: 3000 // 3 seconds }; let config = { ...defaultConfig }; /** * Initialize ADHD focus management system */ export function initADHDFocusManagement(userConfig) { if (userConfig) { config = { ...defaultConfig, ...userConfig }; } // Set up global event listeners for pattern detection if (config.enableInteractionPattern) { setupPatternDetection(); } // Set up hyperfocus monitoring if (config.enableHyperfocusWarning) { setupHyperfocusMonitoring(); } } /** * Enhanced focus handler for ADHD users */ export function handleADHDFocus(element, componentType, interactionType = 'keyboard') { const now = Date.now(); // End previous session if exists if (currentFocusSession) { endFocusSession(); } // Create new focus session currentFocusSession = { startTime: now, element, componentType, interactionType, adhdPatterns: { rapidSwitching: false, hesitation: false, repeatInteractions: 0 } }; // Update attention metrics updateAttentionMetrics(componentType, 'focus', now); // Visual focus enhancement for ADHD enhanceVisualFocus(element); return currentFocusSession; } /** * Handle focus loss with ADHD considerations */ export function handleADHDBlur(componentType) { if (!currentFocusSession) return null; const session = endFocusSession(); const focusTime = Date.now() - session.startTime; // Detect ADHD patterns const patterns = detectADHDPatterns(session, focusTime); // Update metrics if (componentType) { updateAttentionMetrics(componentType, 'blur', Date.now(), focusTime); } // Remove visual enhancements removeVisualEnhancements(session.element); return attentionMetrics.get(componentType || session.componentType) || null; } /** * Smart announcement system with ADHD-friendly debouncing */ export function announceForADHD(componentId, message, priority = 'medium', force = false) { if (!config.enableAnnouncementDebouncing && !force) { createAnnouncement(message, priority); return true; } const now = Date.now(); const lastAnnouncement = announcements.get(componentId) || 0; const delay = priority === 'high' ? config.debounceDelay / 2 : priority === 'medium' ? config.debounceDelay : config.debounceDelay * 2; if (force || (now - lastAnnouncement) > delay) { announcements.set(componentId, now); createAnnouncement(message, priority); return true; } return false; } /** * Keyboard navigation with ADHD-specific enhancements */ export function handleADHDKeyNavigation(event, handlers) { let handled = false; // Enhanced escape handling for ADHD users (quick exit) if (event.key === 'Escape') { event.preventDefault(); handlers.onEscape?.(); announceForADHD('navigation', 'Cancelled. Press h for help.', 'medium', true); handled = true; } // Contextual help (important for ADHD users) else if (event.key === 'h' || event.key === 'H') { event.preventDefault(); handlers.onHelp?.(); handled = true; } // Enhanced navigation else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { event.preventDefault(); handlers.onFocusNext?.(); handled = true; } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { event.preventDefault(); handlers.onFocusPrevious?.(); handled = true; } // Activation else if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handlers.onActivate?.(); handled = true; } // Track interaction patterns if (handled && currentFocusSession) { currentFocusSession.adhdPatterns.repeatInteractions++; } return handled; } /** * Create accessible focus indicators optimized for ADHD */ export function createADHDFocusRing(element, options) { const opts = { color: 'var(--ck-accent, #0066cc)', thickness: 3, offset: 3, animated: false, ...options }; // Remove any existing focus rings removeADHDFocusRing(element); // Apply enhanced focus styling element.style.setProperty('--adhd-focus-color', opts.color); element.style.setProperty('--adhd-focus-thickness', `${opts.thickness}px`); element.style.setProperty('--adhd-focus-offset', `${opts.offset}px`); element.classList.add('ck-adhd-focus'); if (opts.animated && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { element.classList.add('ck-adhd-focus--animated'); } } /** * Remove ADHD focus indicators */ export function removeADHDFocusRing(element) { element.classList.remove('ck-adhd-focus', 'ck-adhd-focus--animated'); element.style.removeProperty('--adhd-focus-color'); element.style.removeProperty('--adhd-focus-thickness'); element.style.removeProperty('--adhd-focus-offset'); } /** * Check if user might be experiencing hyperfocus */ export function isInHyperfocus(componentType) { if (!currentFocusSession || !config.enableHyperfocusWarning) return false; const focusTime = Date.now() - currentFocusSession.startTime; return focusTime > config.hyperfocusThreshold; } /** * Get attention metrics for component */ export function getAttentionMetrics(componentType) { return attentionMetrics.get(componentType) || null; } /** * Get ADHD-specific interaction recommendations */ export function getADHDRecommendations(componentType) { const metrics = attentionMetrics.get(componentType); if (!metrics) return []; const recommendations = []; if (metrics.switchCount > 10) { recommendations.push('Consider taking a short break to reduce task switching'); } if (metrics.hesitationTime > 10000) { recommendations.push('Press h anytime for contextual help and guidance'); } if (metrics.focusTime > config.hyperfocusThreshold) { recommendations.push('You\'ve been focused for a while - consider a short break'); } if (metrics.repeatCount > 5) { recommendations.push('Multiple attempts detected - try using keyboard shortcuts for efficiency'); } return recommendations; } // Internal helper functions function endFocusSession() { if (!currentFocusSession) return null; const session = currentFocusSession; focusHistory.push(session); // Keep only recent history (last 50 sessions) if (focusHistory.length > 50) { focusHistory = focusHistory.slice(-50); } currentFocusSession = null; return session; } function updateAttentionMetrics(componentType, action, timestamp, focusTime) { let metrics = attentionMetrics.get(componentType) || { focusTime: 0, switchCount: 0, hesitationTime: 0, repeatCount: 0, lastActiveTime: 0 }; if (action === 'focus') { metrics.switchCount++; // Detect rapid switching (ADHD pattern) if (timestamp - metrics.lastActiveTime < 2000) { metrics.switchCount += 2; // Weight rapid switching more heavily } } else if (action === 'blur' && focusTime) { metrics.focusTime += focusTime; // Detect hesitation patterns if (focusTime > config.hesitationThreshold && focusTime < 10000) { metrics.hesitationTime += focusTime; } } metrics.lastActiveTime = timestamp; attentionMetrics.set(componentType, metrics); } function detectADHDPatterns(session, focusTime) { return { rapidSwitching: focusTime < 2000 && session.adhdPatterns.repeatInteractions > 2, hesitation: focusTime > config.hesitationThreshold && focusTime < 10000, hyperfocus: focusTime > config.hyperfocusThreshold }; } function enhanceVisualFocus(element) { createADHDFocusRing(element, { animated: true }); } function removeVisualEnhancements(element) { removeADHDFocusRing(element); } function createAnnouncement(message, priority) { const announcement = document.createElement('div'); announcement.setAttribute('aria-live', priority === 'high' ? 'assertive' : 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.className = 'sr-only ck-adhd-announcement'; announcement.textContent = message; document.body.appendChild(announcement); // Remove after announcement is read setTimeout(() => { if (document.body.contains(announcement)) { document.body.removeChild(announcement); } }, 3000); } function setupPatternDetection() { // Monitor for ADHD-specific patterns like rapid clicking let clickCount = 0; let lastClickTime = 0; document.addEventListener('click', () => { const now = Date.now(); if (now - lastClickTime < 1000) { clickCount++; if (clickCount > 5) { announceForADHD('global', 'Rapid clicking detected. Try using keyboard navigation or take a moment to pause.', 'medium'); clickCount = 0; } } else { clickCount = 1; } lastClickTime = now; }); } function setupHyperfocusMonitoring() { setInterval(() => { if (isInHyperfocus()) { announceForADHD('hyperfocus', 'You\'ve been focused for a while. Consider taking a short break when convenient.', 'low'); } }, 5 * 60 * 1000); // Check every 5 minutes } /** * CSS classes for ADHD focus management * These should be added to global styles */ export const adhdFocusCSS = ` .ck-adhd-focus { outline: var(--adhd-focus-thickness, 3px) solid var(--adhd-focus-color, #0066cc) !important; outline-offset: var(--adhd-focus-offset, 3px) !important; box-shadow: 0 0 0 1px white, 0 0 0 calc(var(--adhd-focus-thickness, 3px) + 1px) var(--adhd-focus-color, #0066cc) !important; } .ck-adhd-focus--animated { animation: ck-adhd-focus-pulse 2s ease-in-out infinite; } @keyframes ck-adhd-focus-pulse { 0%, 100% { box-shadow: 0 0 0 1px white, 0 0 0 calc(var(--adhd-focus-thickness, 3px) + 1px) var(--adhd-focus-color, #0066cc); } 50% { box-shadow: 0 0 0 1px white, 0 0 0 calc(var(--adhd-focus-thickness, 3px) + 3px) var(--adhd-focus-color, #0066cc); } } .sr-only, .ck-adhd-announcement { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @media (prefers-reduced-motion: reduce) { .ck-adhd-focus--animated { animation: none; } } @media (prefers-contrast: high) { .ck-adhd-focus { outline-color: currentColor !important; box-shadow: 0 0 0 1px white, 0 0 0 calc(var(--adhd-focus-thickness, 3px) + 1px) currentColor !important; } } `;