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