WCAG 2.1 AA: Focus-Styles, ARIA-Attribute, Tastatur-Navigation, Formvalidierung
- Default :focus-visible auf allen interaktiven Elementen (WCAG 2.4.7) - A11y-Panel: role=group, Esc/Pfeiltasten, Fokus-Management - Checkbox sr-only statt display:none (Screenreader-zugaenglich) - Toggle-Switch Focus-Indikator - aria-required, aria-expanded, aria-haspopup auf Formularen/Dropdowns - Export-Dropdown: role=menu/menuitem/separator - Sidebar: aria-expanded auf Sektionen, aria-hidden auf Chevrons - Globaler Esc-Handler mit korrekter Schliess-Reihenfolge - Formvalidierung: aria-invalid, aria-describedby, Fokus auf Fehler - Notification-Items: role=button, tabindex=0 - Badges: aria-label/aria-hidden fuer Screenreader - SR-Announcement bei Sidebar-Lage-Auswahl Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -59,7 +59,7 @@ const A11yManager = {
|
||||
<path d="M12 8c-3.3 0-6 .5-6 .5v2s2.7-.5 5-.5v3l-3 7h2.5l2.5-5.5 2.5 5.5h2.5l-3-7v-3c2.3 0 5 .5 5 .5v-2S15.3 8 12 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="a11y-panel" id="a11y-panel" style="display:none;">
|
||||
<div class="a11y-panel" id="a11y-panel" role="group" aria-label="Barrierefreiheits-Einstellungen" style="display:none;">
|
||||
<div class="a11y-panel-title">Barrierefreiheit</div>
|
||||
<label class="a11y-option">
|
||||
<input type="checkbox" id="a11y-contrast">
|
||||
@@ -69,7 +69,7 @@ const A11yManager = {
|
||||
<label class="a11y-option">
|
||||
<input type="checkbox" id="a11y-focus">
|
||||
<span class="toggle-switch"></span>
|
||||
<span>Focus-Hervorhebung</span>
|
||||
<span>Verstärkte Focus-Anzeige</span>
|
||||
</label>
|
||||
<label class="a11y-option">
|
||||
<input type="checkbox" id="a11y-fontsize">
|
||||
@@ -108,6 +108,28 @@ const A11yManager = {
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard: Esc schließt, Pfeiltasten navigieren
|
||||
container.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this._isOpen) {
|
||||
e.stopPropagation();
|
||||
this._closePanel();
|
||||
return;
|
||||
}
|
||||
if (!this._isOpen) return;
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const options = Array.from(document.querySelectorAll('.a11y-option input[type="checkbox"]'));
|
||||
const idx = options.indexOf(document.activeElement);
|
||||
let next;
|
||||
if (e.key === 'ArrowDown') {
|
||||
next = idx < options.length - 1 ? idx + 1 : 0;
|
||||
} else {
|
||||
next = idx > 0 ? idx - 1 : options.length - 1;
|
||||
}
|
||||
options[next].focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Einstellungen anwenden + Checkboxen synchronisieren
|
||||
this._apply();
|
||||
this._syncUI();
|
||||
@@ -146,12 +168,19 @@ const A11yManager = {
|
||||
this._isOpen = true;
|
||||
document.getElementById('a11y-panel').style.display = '';
|
||||
document.getElementById('a11y-btn').setAttribute('aria-expanded', 'true');
|
||||
// Fokus auf erste Option setzen
|
||||
requestAnimationFrame(() => {
|
||||
const first = document.querySelector('.a11y-option input[type="checkbox"]');
|
||||
if (first) first.focus();
|
||||
});
|
||||
},
|
||||
|
||||
_closePanel() {
|
||||
this._isOpen = false;
|
||||
document.getElementById('a11y-panel').style.display = 'none';
|
||||
document.getElementById('a11y-btn').setAttribute('aria-expanded', 'false');
|
||||
const btn = document.getElementById('a11y-btn');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -291,7 +320,7 @@ const NotificationCenter = {
|
||||
const timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
const unreadClass = n.read ? '' : ' unread';
|
||||
const icon = n.icon || 'info';
|
||||
return `<div class="notification-item${unreadClass}" onclick="NotificationCenter._handleClick(${n.incident_id})" data-id="${n.incident_id}">
|
||||
return `<div class="notification-item${unreadClass}" onclick="NotificationCenter._handleClick(${n.incident_id})" data-id="${n.incident_id}" role="button" tabindex="0">
|
||||
<div class="notification-item-icon ${icon}">${this._iconSymbol(icon)}</div>
|
||||
<div class="notification-item-body">
|
||||
<div class="notification-item-title">${this._escapeHtml(n.title)}</div>
|
||||
@@ -566,10 +595,27 @@ const App = {
|
||||
this.renderSidebar();
|
||||
},
|
||||
|
||||
_announceForSR(text) {
|
||||
let el = document.getElementById('sr-announcement');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'sr-announcement';
|
||||
el.setAttribute('role', 'status');
|
||||
el.setAttribute('aria-live', 'polite');
|
||||
el.className = 'sr-only';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
el.textContent = '';
|
||||
requestAnimationFrame(() => { el.textContent = text; });
|
||||
},
|
||||
|
||||
async selectIncident(id) {
|
||||
this.closeRefreshHistory();
|
||||
this.currentIncidentId = id;
|
||||
localStorage.setItem('selectedIncidentId', id);
|
||||
// SR-Announcement
|
||||
const inc = this.incidents.find(i => i.id === id);
|
||||
if (inc) this._announceForSR('Lage ausgewählt: ' + inc.title);
|
||||
this.renderSidebar();
|
||||
|
||||
document.getElementById('empty-state').style.display = 'none';
|
||||
@@ -1352,9 +1398,40 @@ const App = {
|
||||
};
|
||||
},
|
||||
|
||||
_clearFormErrors(formEl) {
|
||||
formEl.querySelectorAll('.form-error').forEach(el => el.remove());
|
||||
formEl.querySelectorAll('[aria-invalid]').forEach(el => {
|
||||
el.removeAttribute('aria-invalid');
|
||||
el.removeAttribute('aria-describedby');
|
||||
});
|
||||
},
|
||||
|
||||
_showFieldError(field, message) {
|
||||
field.setAttribute('aria-invalid', 'true');
|
||||
const errorId = field.id + '-error';
|
||||
field.setAttribute('aria-describedby', errorId);
|
||||
const errorEl = document.createElement('div');
|
||||
errorEl.className = 'form-error';
|
||||
errorEl.id = errorId;
|
||||
errorEl.setAttribute('role', 'alert');
|
||||
errorEl.textContent = message;
|
||||
field.parentNode.appendChild(errorEl);
|
||||
},
|
||||
|
||||
async handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('modal-new-submit');
|
||||
const form = document.getElementById('new-incident-form');
|
||||
this._clearFormErrors(form);
|
||||
|
||||
// Validierung
|
||||
const titleField = document.getElementById('inc-title');
|
||||
if (!titleField.value.trim()) {
|
||||
this._showFieldError(titleField, 'Bitte einen Titel eingeben.');
|
||||
titleField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -1926,12 +2003,18 @@ const App = {
|
||||
event.stopPropagation();
|
||||
const menu = document.getElementById('export-dropdown-menu');
|
||||
if (!menu) return;
|
||||
menu.classList.toggle('show');
|
||||
const isOpen = menu.classList.toggle('show');
|
||||
const btn = menu.previousElementSibling;
|
||||
if (btn) btn.setAttribute('aria-expanded', String(isOpen));
|
||||
},
|
||||
|
||||
_closeExportDropdown() {
|
||||
const menu = document.getElementById('export-dropdown-menu');
|
||||
if (menu) menu.classList.remove('show');
|
||||
if (menu) {
|
||||
menu.classList.remove('show');
|
||||
const btn = menu.previousElementSibling;
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
},
|
||||
|
||||
async exportIncident(format, scope) {
|
||||
@@ -2013,12 +2096,17 @@ const App = {
|
||||
|
||||
async submitFeedback(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('feedback-form');
|
||||
this._clearFormErrors(form);
|
||||
|
||||
const btn = document.getElementById('fb-submit-btn');
|
||||
const category = document.getElementById('fb-category').value;
|
||||
const message = document.getElementById('fb-message').value.trim();
|
||||
const msgField = document.getElementById('fb-message');
|
||||
const message = msgField.value.trim();
|
||||
|
||||
if (message.length < 10) {
|
||||
UI.showToast('Bitte mindestens 10 Zeichen eingeben.', 'warning');
|
||||
this._showFieldError(msgField, 'Bitte mindestens 10 Zeichen eingeben.');
|
||||
msgField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2047,6 +2135,9 @@ const App = {
|
||||
if (chevron) {
|
||||
chevron.classList.toggle('open', isHidden);
|
||||
}
|
||||
// aria-expanded auf dem Section-Title synchronisieren
|
||||
const title = chevron ? chevron.closest('.sidebar-section-title') : null;
|
||||
if (title) title.setAttribute('aria-expanded', String(isHidden));
|
||||
},
|
||||
|
||||
// === Quellenverwaltung ===
|
||||
@@ -2963,6 +3054,27 @@ document.addEventListener('keydown', (e) => {
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Schließ-Reihenfolge: A11y-Panel > Notification-Panel > Export-Dropdown > FC-Dropdown > Modals
|
||||
if (A11yManager._isOpen) {
|
||||
A11yManager._closePanel();
|
||||
return;
|
||||
}
|
||||
if (NotificationCenter._isOpen) {
|
||||
NotificationCenter.close();
|
||||
return;
|
||||
}
|
||||
const exportMenu = document.getElementById('export-dropdown-menu');
|
||||
if (exportMenu && exportMenu.classList.contains('show')) {
|
||||
App._closeExportDropdown();
|
||||
return;
|
||||
}
|
||||
const fcMenu = document.querySelector('.fc-dropdown-menu.open');
|
||||
if (fcMenu) {
|
||||
fcMenu.classList.remove('open');
|
||||
const fcBtn = fcMenu.previousElementSibling;
|
||||
if (fcBtn) fcBtn.setAttribute('aria-expanded', 'false');
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('.modal-overlay.active').forEach(m => {
|
||||
closeModal(m.id);
|
||||
});
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren