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
JavaScript
/**
* 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;
}
}
`;