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:
@@ -2539,6 +2539,11 @@ a:hover {
|
|||||||
transition: transform 0.2s, background 0.2s;
|
transition: transform 0.2s, background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-label input:focus-visible + .toggle-switch {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-label input:checked + .toggle-switch {
|
.toggle-label input:checked + .toggle-switch {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
@@ -3786,6 +3791,21 @@ a:hover {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Default Focus-Visible fuer alle interaktiven Elemente (WCAG 2.4.7) === */
|
||||||
|
a:focus-visible, button:focus-visible, input:focus-visible,
|
||||||
|
select:focus-visible, textarea:focus-visible,
|
||||||
|
[tabindex]:focus-visible, [role="button"]:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form-Fehler (Accessibility) */
|
||||||
|
.form-error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--error);
|
||||||
|
margin-top: var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
/* prefers-reduced-motion: alle Animationen deaktivieren */
|
/* prefers-reduced-motion: alle Animationen deaktivieren */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
@@ -3847,10 +3867,22 @@ a:hover {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.a11y-option input[type="checkbox"] { display: none; }
|
.a11y-option input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
.a11y-option .toggle-switch {
|
.a11y-option .toggle-switch {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.a11y-option input:focus-visible + .toggle-switch {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
.a11y-option input:checked + .toggle-switch {
|
.a11y-option input:checked + .toggle-switch {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
@@ -4021,16 +4053,17 @@ a:hover {
|
|||||||
--input-border: #94A3B8;
|
--input-border: #94A3B8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === A11y: Focus-Hervorhebung === */
|
/* === A11y: Verstaerkte Focus-Anzeige === */
|
||||||
[data-a11y-focus="true"] a:focus,
|
[data-a11y-focus="true"] a:focus-visible,
|
||||||
[data-a11y-focus="true"] button:focus,
|
[data-a11y-focus="true"] button:focus-visible,
|
||||||
[data-a11y-focus="true"] input:focus,
|
[data-a11y-focus="true"] input:focus-visible,
|
||||||
[data-a11y-focus="true"] select:focus,
|
[data-a11y-focus="true"] select:focus-visible,
|
||||||
[data-a11y-focus="true"] textarea:focus,
|
[data-a11y-focus="true"] textarea:focus-visible,
|
||||||
[data-a11y-focus="true"] [tabindex]:focus,
|
[data-a11y-focus="true"] [tabindex]:focus-visible,
|
||||||
[data-a11y-focus="true"] [role="button"]:focus {
|
[data-a11y-focus="true"] [role="button"]:focus-visible {
|
||||||
outline: 3px solid var(--accent) !important;
|
outline: 3px solid var(--accent) !important;
|
||||||
outline-offset: 2px !important;
|
outline-offset: 2px !important;
|
||||||
|
box-shadow: 0 0 0 4px rgba(200, 168, 81, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === A11y: Größere Schrift === */
|
/* === A11y: Größere Schrift === */
|
||||||
|
|||||||
@@ -53,8 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-incidents')" role="button" tabindex="0">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-incidents')" role="button" tabindex="0" aria-expanded="true">
|
||||||
<span class="sidebar-chevron" id="chevron-active-incidents">▾</span>
|
<span class="sidebar-chevron" id="chevron-active-incidents" aria-hidden="true">▾</span>
|
||||||
Aktive Lagen
|
Aktive Lagen
|
||||||
<span class="sidebar-section-count" id="count-active-incidents"></span>
|
<span class="sidebar-section-count" id="count-active-incidents"></span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-research')" role="button" tabindex="0">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-research')" role="button" tabindex="0" aria-expanded="true">
|
||||||
<span class="sidebar-chevron" id="chevron-active-research">▾</span>
|
<span class="sidebar-chevron" id="chevron-active-research" aria-hidden="true">▾</span>
|
||||||
Aktive Recherchen
|
Aktive Recherchen
|
||||||
<span class="sidebar-section-count" id="count-active-research"></span>
|
<span class="sidebar-section-count" id="count-active-research"></span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -71,8 +71,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('archived-incidents')" role="button" tabindex="0">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('archived-incidents')" role="button" tabindex="0" aria-expanded="false">
|
||||||
<span class="sidebar-chevron" id="chevron-archived-incidents">▾</span>
|
<span class="sidebar-chevron" id="chevron-archived-incidents" aria-hidden="true">▾</span>
|
||||||
Archiv
|
Archiv
|
||||||
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -111,15 +111,15 @@
|
|||||||
<button class="btn btn-primary btn-small" id="refresh-btn">Aktualisieren</button>
|
<button class="btn btn-primary btn-small" id="refresh-btn">Aktualisieren</button>
|
||||||
<button class="btn btn-secondary btn-small" id="edit-incident-btn">Bearbeiten</button>
|
<button class="btn btn-secondary btn-small" id="edit-incident-btn">Bearbeiten</button>
|
||||||
<div class="export-dropdown" id="export-dropdown">
|
<div class="export-dropdown" id="export-dropdown">
|
||||||
<button class="btn btn-secondary btn-small" onclick="App.toggleExportDropdown(event)">Exportieren ▾</button>
|
<button class="btn btn-secondary btn-small" onclick="App.toggleExportDropdown(event)" aria-expanded="false" aria-haspopup="true">Exportieren ▾</button>
|
||||||
<div class="export-dropdown-menu" id="export-dropdown-menu">
|
<div class="export-dropdown-menu" id="export-dropdown-menu" role="menu">
|
||||||
<button class="export-dropdown-item" onclick="App.exportIncident('md','report')">Lagebericht (Markdown)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','report')">Lagebericht (Markdown)</button>
|
||||||
<button class="export-dropdown-item" onclick="App.exportIncident('json','report')">Lagebericht (JSON)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','report')">Lagebericht (JSON)</button>
|
||||||
<hr class="export-dropdown-divider">
|
<hr class="export-dropdown-divider" role="separator">
|
||||||
<button class="export-dropdown-item" onclick="App.exportIncident('md','full')">Vollexport (Markdown)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','full')">Vollexport (Markdown)</button>
|
||||||
<button class="export-dropdown-item" onclick="App.exportIncident('json','full')">Vollexport (JSON)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','full')">Vollexport (JSON)</button>
|
||||||
<hr class="export-dropdown-divider">
|
<hr class="export-dropdown-divider" role="separator">
|
||||||
<button class="export-dropdown-item" onclick="App.printIncident()">Drucken / PDF</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.printIncident()">Drucken / PDF</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card incident-analysis-summary">
|
<div class="card incident-analysis-summary">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
|
||||||
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="summary-content">
|
<div id="summary-content">
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" onclick="openContentModal('Faktencheck', 'factcheck-list')">Faktencheck</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Faktencheck', 'factcheck-list')">Faktencheck</div>
|
||||||
<div class="fc-filter-bar" id="fc-filters"></div>
|
<div class="fc-filter-bar" id="fc-filters"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="factcheck-list" id="factcheck-list">
|
<div class="factcheck-list" id="factcheck-list">
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card timeline-card">
|
<div class="card timeline-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" onclick="openContentModal('Ereignis-Timeline', 'timeline')">Ereignis-Timeline</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Ereignis-Timeline', 'timeline')">Ereignis-Timeline</div>
|
||||||
<div class="ht-controls">
|
<div class="ht-controls">
|
||||||
<div class="ht-filter-group">
|
<div class="ht-filter-group">
|
||||||
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
|
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-title">Titel des Vorfalls</label>
|
<label for="inc-title">Titel des Vorfalls</label>
|
||||||
<input type="text" id="inc-title" required placeholder="z.B. Explosion in Madrid">
|
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-description">Beschreibung / Kontext</label>
|
<label for="inc-description">Beschreibung / Kontext</label>
|
||||||
@@ -544,7 +544,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fb-message">Nachricht</label>
|
<label for="fb-message">Nachricht</label>
|
||||||
<textarea id="fb-message" required minlength="10" maxlength="5000" rows="6" placeholder="Beschreibe dein Anliegen (mind. 10 Zeichen)..."></textarea>
|
<textarea id="fb-message" required aria-required="true" minlength="10" maxlength="5000" rows="6" placeholder="Beschreibe dein Anliegen (mind. 10 Zeichen)..."></textarea>
|
||||||
<div class="form-hint"><span id="fb-char-count">0</span> / 5.000 Zeichen</div>
|
<div class="form-hint"><span id="fb-char-count">0</span> / 5.000 Zeichen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<form id="email-form">
|
<form id="email-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">E-Mail-Adresse</label>
|
<label for="email">E-Mail-Adresse</label>
|
||||||
<input type="email" id="email" name="email" autocomplete="email" required placeholder="name@organisation.de">
|
<input type="email" id="email" name="email" autocomplete="email" required aria-required="true" placeholder="name@organisation.de">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="email-btn">Anmelden</button>
|
<button type="submit" class="btn btn-primary btn-full" id="email-btn">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="code">Code eingeben</label>
|
<label for="code">Code eingeben</label>
|
||||||
<input type="text" id="code" name="code" autocomplete="one-time-code" required
|
<input type="text" id="code" name="code" autocomplete="one-time-code" required aria-required="true"
|
||||||
placeholder="000000" maxlength="6" pattern="[0-9]{6}"
|
placeholder="000000" maxlength="6" pattern="[0-9]{6}"
|
||||||
style="text-align:center; font-size:24px; letter-spacing:8px; font-family:monospace;">
|
style="text-align:center; font-size:24px; letter-spacing:8px; font-family:monospace;">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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"/>
|
<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>
|
</svg>
|
||||||
</button>
|
</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>
|
<div class="a11y-panel-title">Barrierefreiheit</div>
|
||||||
<label class="a11y-option">
|
<label class="a11y-option">
|
||||||
<input type="checkbox" id="a11y-contrast">
|
<input type="checkbox" id="a11y-contrast">
|
||||||
@@ -69,7 +69,7 @@ const A11yManager = {
|
|||||||
<label class="a11y-option">
|
<label class="a11y-option">
|
||||||
<input type="checkbox" id="a11y-focus">
|
<input type="checkbox" id="a11y-focus">
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span>Focus-Hervorhebung</span>
|
<span>Verstärkte Focus-Anzeige</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="a11y-option">
|
<label class="a11y-option">
|
||||||
<input type="checkbox" id="a11y-fontsize">
|
<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
|
// Einstellungen anwenden + Checkboxen synchronisieren
|
||||||
this._apply();
|
this._apply();
|
||||||
this._syncUI();
|
this._syncUI();
|
||||||
@@ -146,12 +168,19 @@ const A11yManager = {
|
|||||||
this._isOpen = true;
|
this._isOpen = true;
|
||||||
document.getElementById('a11y-panel').style.display = '';
|
document.getElementById('a11y-panel').style.display = '';
|
||||||
document.getElementById('a11y-btn').setAttribute('aria-expanded', 'true');
|
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() {
|
_closePanel() {
|
||||||
this._isOpen = false;
|
this._isOpen = false;
|
||||||
document.getElementById('a11y-panel').style.display = 'none';
|
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 timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||||
const unreadClass = n.read ? '' : ' unread';
|
const unreadClass = n.read ? '' : ' unread';
|
||||||
const icon = n.icon || 'info';
|
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-icon ${icon}">${this._iconSymbol(icon)}</div>
|
||||||
<div class="notification-item-body">
|
<div class="notification-item-body">
|
||||||
<div class="notification-item-title">${this._escapeHtml(n.title)}</div>
|
<div class="notification-item-title">${this._escapeHtml(n.title)}</div>
|
||||||
@@ -566,10 +595,27 @@ const App = {
|
|||||||
this.renderSidebar();
|
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) {
|
async selectIncident(id) {
|
||||||
this.closeRefreshHistory();
|
this.closeRefreshHistory();
|
||||||
this.currentIncidentId = id;
|
this.currentIncidentId = id;
|
||||||
localStorage.setItem('selectedIncidentId', 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();
|
this.renderSidebar();
|
||||||
|
|
||||||
document.getElementById('empty-state').style.display = 'none';
|
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) {
|
async handleFormSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const submitBtn = document.getElementById('modal-new-submit');
|
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;
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1926,12 +2003,18 @@ const App = {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const menu = document.getElementById('export-dropdown-menu');
|
const menu = document.getElementById('export-dropdown-menu');
|
||||||
if (!menu) return;
|
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() {
|
_closeExportDropdown() {
|
||||||
const menu = document.getElementById('export-dropdown-menu');
|
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) {
|
async exportIncident(format, scope) {
|
||||||
@@ -2013,12 +2096,17 @@ const App = {
|
|||||||
|
|
||||||
async submitFeedback(e) {
|
async submitFeedback(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const form = document.getElementById('feedback-form');
|
||||||
|
this._clearFormErrors(form);
|
||||||
|
|
||||||
const btn = document.getElementById('fb-submit-btn');
|
const btn = document.getElementById('fb-submit-btn');
|
||||||
const category = document.getElementById('fb-category').value;
|
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) {
|
if (message.length < 10) {
|
||||||
UI.showToast('Bitte mindestens 10 Zeichen eingeben.', 'warning');
|
this._showFieldError(msgField, 'Bitte mindestens 10 Zeichen eingeben.');
|
||||||
|
msgField.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2047,6 +2135,9 @@ const App = {
|
|||||||
if (chevron) {
|
if (chevron) {
|
||||||
chevron.classList.toggle('open', isHidden);
|
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 ===
|
// === Quellenverwaltung ===
|
||||||
@@ -2963,6 +3054,27 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
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 => {
|
document.querySelectorAll('.modal-overlay.active').forEach(m => {
|
||||||
closeModal(m.id);
|
closeModal(m.id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ const UI = {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="incident-item ${activeClass}" data-id="${incident.id}" onclick="App.selectIncident(${incident.id})" role="button" tabindex="0">
|
<div class="incident-item ${activeClass}" data-id="${incident.id}" onclick="App.selectIncident(${incident.id})" role="button" tabindex="0">
|
||||||
<span class="incident-dot ${dotClass}" id="dot-${incident.id}"></span>
|
<span class="incident-dot ${dotClass}" id="dot-${incident.id}" aria-hidden="true"></span>
|
||||||
<div style="flex:1;min-width:0;">
|
<div style="flex:1;min-width:0;">
|
||||||
<div class="incident-name">${this.escape(incident.title)}</div>
|
<div class="incident-name">${this.escape(incident.title)}</div>
|
||||||
<div class="incident-meta">${incident.article_count} Artikel · ${this.escape(creator)}</div>
|
<div class="incident-meta">${incident.article_count} Artikel · ${this.escape(creator)}</div>
|
||||||
</div>
|
</div>
|
||||||
${incident.visibility === 'private' ? '<span class="badge badge-private" style="font-size:9px;">PRIVAT</span>' : ''}
|
${incident.visibility === 'private' ? '<span class="badge badge-private" style="font-size:9px;" aria-label="Private Lage">PRIVAT</span>' : ''}
|
||||||
${incident.refresh_mode === 'auto' ? '<span class="badge badge-auto" title="Auto-Refresh aktiv">↻</span>' : ''}
|
${incident.refresh_mode === 'auto' ? '<span class="badge badge-auto" role="img" aria-label="Auto-Refresh aktiv">↻</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ const UI = {
|
|||||||
</label>`;
|
</label>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<button class="fc-dropdown-toggle" onclick="App.toggleFcDropdown(event)">Filter</button>
|
return `<button class="fc-dropdown-toggle" onclick="App.toggleFcDropdown(event)" aria-haspopup="true" aria-expanded="false">Filter</button>
|
||||||
<div class="fc-dropdown-menu" id="fc-dropdown-menu">${items}</div>`;
|
<div class="fc-dropdown-menu" id="fc-dropdown-menu">${items}</div>`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren