@casoon/dragonfly
Version:
Modular, lightweight CSS framework and design system for modern web projects. Optimized for Astro JS, LightningCSS and Container Queries with @layer-based architecture and comprehensive accessibility.
343 lines (300 loc) • 10.9 kB
JavaScript
/**
* Accessibility Helper
*
* JavaScript-Hilfsfunktionen für verbesserte Barrierefreiheit.
* Enthält Funktionen für Keyboard-Navigation, Screenreader-Support und Kontrast.
*/
(() => {
// Konstanten für Klassenamen
const KEYBOARD_USER_CLASS = 'keyboard-user';
const HIGH_CONTRAST_CLASS = 'high-contrast-mode';
const REDUCED_MOTION_CLASS = 'reduced-motion';
const STORAGE_KEY_CONTRAST = 'casoon-dragonfly-high-contrast';
const STORAGE_KEY_MOTION = 'casoon-dragonfly-reduced-motion';
/**
* Erkennt, ob der Benutzer per Tastatur navigiert und fügt eine Klasse hinzu.
* Dies verbessert die Sichtbarkeit des Fokusrings nur für Tastaturbenutzer.
*/
function setupKeyboardDetection() {
const html = document.documentElement;
let isUsingMouse = false;
// Bei Mausbewegung wird angenommen, dass die Maus verwendet wird
document.addEventListener(
'mousemove',
() => {
isUsingMouse = true;
html.classList.remove(KEYBOARD_USER_CLASS);
},
true
);
// Bei Tastendruck wird angenommen, dass die Tastatur verwendet wird
document.addEventListener(
'keydown',
(event) => {
// Tab-Taste oder Pfeil-Tasten gelten als Indikator für Tastaturnavigation
if (
event.key === 'Tab' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
isUsingMouse = false;
html.classList.add(KEYBOARD_USER_CLASS);
}
},
true
);
// Fokus-Events überwachen
document.addEventListener('focusin', () => {
if (!isUsingMouse) {
html.classList.add(KEYBOARD_USER_CLASS);
}
});
}
/**
* Richtet Skip-Links ein für die Tastatur-Navigation.
* Dies ermöglicht es Tastatur-Benutzern, direkt zum Hauptinhalt zu springen.
*/
function setupSkipLinks() {
// Skip-Links automatisch erkennen
const skipLinks = document.querySelectorAll('.skip-link');
for (const link of skipLinks) {
// Wenn der Link ein href-Attribut hat, das auf eine ID verweist
if (link.getAttribute('href')?.startsWith('#')) {
const targetId = link.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Tabindex hinzufügen, falls nicht vorhanden
if (!targetElement.hasAttribute('tabindex')) {
targetElement.setAttribute('tabindex', '-1');
}
// Event-Listener für den Skip-Link
link.addEventListener('click', (event) => {
event.preventDefault();
targetElement.focus();
targetElement.scrollIntoView({ behavior: 'smooth' });
});
}
}
}
// Falls keine Skip-Links vorhanden sind, automatisch einen erstellen
if (skipLinks.length === 0) {
const mainContent = document.querySelector('main, [role="main"]');
if (mainContent && !document.querySelector('.skip-link')) {
// Skip-Link erstellen
const skipLink = document.createElement('a');
skipLink.classList.add('skip-link');
skipLink.href = '#main-content';
skipLink.textContent = 'Zum Hauptinhalt springen';
// ID zum Hauptinhalt hinzufügen, falls nicht vorhanden
if (!mainContent.id) {
mainContent.id = 'main-content';
}
// Tabindex hinzufügen, falls nicht vorhanden
if (!mainContent.hasAttribute('tabindex')) {
mainContent.setAttribute('tabindex', '-1');
}
// Skip-Link am Anfang des body einfügen
document.body.insertBefore(skipLink, document.body.firstChild);
// Event-Listener für den Skip-Link
skipLink.addEventListener('click', (event) => {
event.preventDefault();
mainContent.focus();
mainContent.scrollIntoView({ behavior: 'smooth' });
});
}
}
}
/**
* Aktiviert/Deaktiviert den Hochkontrast-Modus.
* @param {boolean} enable - Ob der Hochkontrast-Modus aktiviert werden soll.
*/
function toggleHighContrast(enable) {
let shouldEnable = enable;
if (typeof shouldEnable !== 'boolean') {
// Toggle-Modus, wenn kein Parameter übergeben wird
shouldEnable = !document.documentElement.classList.contains(HIGH_CONTRAST_CLASS);
}
if (shouldEnable) {
document.documentElement.classList.add(HIGH_CONTRAST_CLASS);
} else {
document.documentElement.classList.remove(HIGH_CONTRAST_CLASS);
}
// Speichern der Einstellung
try {
localStorage.setItem(STORAGE_KEY_CONTRAST, shouldEnable ? 'true' : 'false');
} catch (error) {
console.warn('Error saving high contrast setting to localStorage:', error);
}
// Event auslösen
document.dispatchEvent(
new CustomEvent('casoon-dragonfly-contrast-change', {
detail: { highContrast: shouldEnable },
})
);
}
/**
* Aktiviert/Deaktiviert den Modus für reduzierte Bewegung.
* @param {boolean} enable - Ob der Modus für reduzierte Bewegung aktiviert werden soll.
*/
function toggleReducedMotion(enable) {
let shouldEnable = enable;
if (typeof shouldEnable !== 'boolean') {
// Toggle-Modus, wenn kein Parameter übergeben wird
shouldEnable = !document.documentElement.classList.contains(REDUCED_MOTION_CLASS);
}
if (shouldEnable) {
document.documentElement.classList.add(REDUCED_MOTION_CLASS);
} else {
document.documentElement.classList.remove(REDUCED_MOTION_CLASS);
}
// Speichern der Einstellung
try {
localStorage.setItem(STORAGE_KEY_MOTION, shouldEnable ? 'true' : 'false');
} catch (error) {
console.warn('Error saving reduced motion setting to localStorage:', error);
}
// Event auslösen
document.dispatchEvent(
new CustomEvent('casoon-dragonfly-motion-change', {
detail: { reducedMotion: shouldEnable },
})
);
}
/**
* Überwacht aria-live-Regionen und verbessert deren Zugänglichkeit.
*/
function setupLiveRegions() {
// Alle aria-live-Regionen finden
const liveRegions = document.querySelectorAll('[aria-live]');
// MutationObserver für jede Region erstellen
for (const region of liveRegions) {
const observer = new MutationObserver((mutations) => {
// Bei Änderungen ggf. zusätzliche Accessibility-Verbesserungen vornehmen
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Für hinzugefügte Nodes sicherstellen, dass sie zugänglich sind
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
ensureAccessibleElement(node);
}
}
}
}
});
// Observer starten
observer.observe(region, { childList: true, subtree: true });
}
}
/**
* Stellt sicher, dass ein Element für Screenreader zugänglich ist.
* @param {Element} element - Das zu prüfende Element.
*/
function ensureAccessibleElement(element) {
// Prüfen, ob das Element einen Button darstellt, aber keine Rolle hat
if (
element.tagName !== 'BUTTON' &&
element.getAttribute('onclick') !== null &&
!element.getAttribute('role')
) {
element.setAttribute('role', 'button');
}
// Prüfen, ob das Element ein Tab ist, aber keine Rolle hat
if (element.classList.contains('tab') && !element.getAttribute('role')) {
element.setAttribute('role', 'tab');
}
// Prüfen, ob das Element ein Modal ist, aber keine Rolle hat
if (element.classList.contains('modal') && !element.getAttribute('role')) {
element.setAttribute('role', 'dialog');
element.setAttribute('aria-modal', 'true');
}
// Rekursiv für alle Kindelemente prüfen
for (const child of Array.from(element.children)) {
ensureAccessibleElement(child);
}
}
/**
* Stellt sicher, dass interaktive Elemente per Tastatur bedienbar sind.
*/
function ensureKeyboardAccessibility() {
// Elemente mit onClick-Handler, aber ohne Tastatur-Zugriff
for (const element of document.querySelectorAll(
'[onclick]:not(button):not(a):not([tabindex])'
)) {
element.setAttribute('tabindex', '0');
element.setAttribute('role', 'button');
// Keyboard-Event hinzufügen
element.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
element.click();
}
});
}
// Clickable elements without tabindex
for (const element of document.querySelectorAll(
'.clickable:not([tabindex]), .keyboard-focusable:not([tabindex])'
)) {
element.setAttribute('tabindex', '0');
}
}
/**
* Initialisiert alle Accessibility-Funktionen.
*/
function initialize() {
// Tastatur-Navigations-Erkennung einrichten
setupKeyboardDetection();
// Skip-Links einrichten
setupSkipLinks();
// Live-Regionen überwachen
setupLiveRegions();
// Tastatur-Bedienbarkeit sicherstellen
ensureKeyboardAccessibility();
// Gespeicherte Einstellungen wiederherstellen
try {
const savedContrast = localStorage.getItem(STORAGE_KEY_CONTRAST);
if (savedContrast === 'true') {
toggleHighContrast(true);
}
const savedMotion = localStorage.getItem(STORAGE_KEY_MOTION);
if (savedMotion === 'true') {
toggleReducedMotion(true);
} else {
// Auf Systemeinstellung für reduzierte Bewegung prüfen
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
toggleReducedMotion(true);
}
}
} catch (error) {
console.warn('Error reading accessibility settings from localStorage:', error);
}
// Auf Änderungen der Systemeinstellung für reduzierte Bewegung reagieren
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (event) => {
// Nur anwenden, wenn keine benutzerdefinierte Einstellung gespeichert ist
try {
const savedMotion = localStorage.getItem(STORAGE_KEY_MOTION);
if (savedMotion === null) {
toggleReducedMotion(event.matches);
}
} catch (error) {
toggleReducedMotion(event.matches);
}
});
}
// Initialisierung
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
}
// API exportieren
window.CasoonUIAccessibility = {
toggleHighContrast,
toggleReducedMotion,
ensureAccessibleElement,
ensureKeyboardAccessibility,
};
})();