diff --git a/public/components/modal.js b/public/components/modal.js
index 7544985..4dcb51f 100644
--- a/public/components/modal.js
+++ b/public/components/modal.js
@@ -591,6 +591,7 @@ function _validateField(input) {
const hasValue = input.value.trim().length > 0;
group?.classList.toggle('form-field--error', !hasValue);
group?.classList.toggle('form-field--valid', hasValue);
+ input.setAttribute('aria-invalid', String(!hasValue));
return hasValue;
}
diff --git a/public/locales/de.json b/public/locales/de.json
index aba3243..5a47964 100644
--- a/public/locales/de.json
+++ b/public/locales/de.json
@@ -170,7 +170,8 @@
"filterGroupStatus": "Status",
"filterGroupPriority": "Priorität",
"filterGroupPerson": "Person",
- "filterClearAll": "Alle Filter zurücksetzen"
+ "filterClearAll": "Alle Filter zurücksetzen",
+ "navLabelOverdue": "Aufgaben, {{count}} überfällig"
},
"shopping": {
"title": "Einkauf",
@@ -299,7 +300,17 @@
"locationPlaceholder": "Optional",
"assignedLabel": "Zugewiesen an",
"assignedNobody": "- Niemand -",
- "colorLabel": "Farbe {{color}}",
+ "colorLabel": "Farbe",
+ "colorBlue": "Blau",
+ "colorGreen": "Grün",
+ "colorOrange": "Orange",
+ "colorRed": "Rot",
+ "colorPurple": "Lila",
+ "colorCoral": "Korall",
+ "colorSkyBlue": "Hellblau",
+ "colorYellow": "Gelb",
+ "colorGray": "Grau",
+ "colorCyan": "Cyan",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Optional…",
"popupEdit": "Bearbeiten",
@@ -379,7 +390,15 @@
"formatLink": "Link",
"formatCode": "Code",
"formatQuote": "Zitat",
- "formatDivider": "Trennlinie"
+ "formatDivider": "Trennlinie",
+ "colorYellow": "Gelb",
+ "colorAmber": "Hellgelb",
+ "colorGreen": "Grün",
+ "colorTeal": "Türkis",
+ "colorBlue": "Blau",
+ "colorPurple": "Lila",
+ "colorOrange": "Orange",
+ "colorWhite": "Weiß"
},
"contacts": {
"title": "Kontakte",
diff --git a/public/pages/calendar.js b/public/pages/calendar.js
index 24ddb05..7e86cf4 100644
--- a/public/pages/calendar.js
+++ b/public/pages/calendar.js
@@ -46,6 +46,19 @@ const EVENT_COLORS = [
'#8E8E93', '#30B0C7',
];
+const EVENT_COLOR_NAMES = () => ({
+ '#007AFF': t('calendar.colorBlue'),
+ '#34C759': t('calendar.colorGreen'),
+ '#FF9500': t('calendar.colorOrange'),
+ '#FF3B30': t('calendar.colorRed'),
+ '#AF52DE': t('calendar.colorPurple'),
+ '#FF6B35': t('calendar.colorCoral'),
+ '#5AC8FA': t('calendar.colorSkyBlue'),
+ '#FFCC00': t('calendar.colorYellow'),
+ '#8E8E93': t('calendar.colorGray'),
+ '#30B0C7': t('calendar.colorCyan'),
+});
+
const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht
/**
@@ -843,15 +856,36 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
const selectedColor = isEdit ? (event?.color || EVENT_COLORS[0]) : EVENT_COLORS[0];
- // Farb-Auswahl
- panel.querySelectorAll('.color-swatch').forEach((sw) => {
- sw.addEventListener('click', () => {
- panel.querySelectorAll('.color-swatch').forEach((s) => s.classList.remove('color-swatch--active'));
- sw.classList.add('color-swatch--active');
+ // Farb-Auswahl: Auswahl + ARIA + Keyboard (Roving Tabindex)
+ function selectSwatch(target) {
+ panel.querySelectorAll('.color-swatch').forEach((s) => {
+ s.classList.remove('color-swatch--active');
+ s.setAttribute('aria-checked', 'false');
+ s.setAttribute('tabindex', '-1');
});
- });
+ target.classList.add('color-swatch--active');
+ target.setAttribute('aria-checked', 'true');
+ target.setAttribute('tabindex', '0');
+ }
panel.querySelectorAll('.color-swatch').forEach((sw) => {
- if (sw.dataset.color === selectedColor) sw.classList.add('color-swatch--active');
+ if (sw.dataset.color === selectedColor) selectSwatch(sw);
+ sw.addEventListener('click', () => { selectSwatch(sw); sw.focus(); });
+ sw.addEventListener('keydown', (e) => {
+ const swatches = [...panel.querySelectorAll('.color-swatch')];
+ const idx = swatches.indexOf(sw);
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ const next = swatches[(idx + 1) % swatches.length];
+ selectSwatch(next); next.focus();
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ const prev = swatches[(idx - 1 + swatches.length) % swatches.length];
+ selectSwatch(prev); prev.focus();
+ } else if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ selectSwatch(sw);
+ }
+ });
});
// Ganztägig-Toggle
@@ -957,11 +991,14 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
diff --git a/public/pages/login.js b/public/pages/login.js
index 9f72f54..95aef49 100644
--- a/public/pages/login.js
+++ b/public/pages/login.js
@@ -86,6 +86,8 @@ export async function render(container) {
usernameGroup.classList.toggle('form-group--error', !username);
passwordGroup.classList.toggle('form-group--error', !password);
+ usernameInput.setAttribute('aria-invalid', String(!username));
+ passwordInput.setAttribute('aria-invalid', String(!password));
if (!username || !password) {
if (!username) usernameInput.focus();
@@ -117,11 +119,13 @@ export async function render(container) {
}
});
- form.querySelector('#username').addEventListener('input', () => {
- form.querySelector('#username').closest('.form-group').classList.remove('form-group--error');
+ form.querySelector('#username').addEventListener('input', (e) => {
+ e.currentTarget.closest('.form-group').classList.remove('form-group--error');
+ e.currentTarget.removeAttribute('aria-invalid');
});
- form.querySelector('#password').addEventListener('input', () => {
- form.querySelector('#password').closest('.form-group').classList.remove('form-group--error');
+ form.querySelector('#password').addEventListener('input', (e) => {
+ e.currentTarget.closest('.form-group').classList.remove('form-group--error');
+ e.currentTarget.removeAttribute('aria-invalid');
});
}
diff --git a/public/pages/notes.js b/public/pages/notes.js
index c19e9ec..da552f2 100644
--- a/public/pages/notes.js
+++ b/public/pages/notes.js
@@ -19,6 +19,17 @@ const NOTE_COLORS = [
'#90CAF9', '#CE93D8', '#FFAB91', '#FFFFFF',
];
+const NOTE_COLOR_NAMES = () => ({
+ '#FFEB3B': t('notes.colorYellow'),
+ '#FFD54F': t('notes.colorAmber'),
+ '#A5D6A7': t('notes.colorGreen'),
+ '#80DEEA': t('notes.colorTeal'),
+ '#90CAF9': t('notes.colorBlue'),
+ '#CE93D8': t('notes.colorPurple'),
+ '#FFAB91': t('notes.colorOrange'),
+ '#FFFFFF': t('notes.colorWhite'),
+});
+
// --------------------------------------------------------
// State
// --------------------------------------------------------
@@ -368,13 +379,16 @@ function openNoteModal({ mode, note = null }) {
style="resize:vertical;">${esc(isEdit ? note.content : '')}
@@ -396,11 +410,34 @@ function openNoteModal({ mode, note = null }) {
content,
size: 'md',
onSave(panel) {
- // Farb-Swatch
+ // Farb-Swatch: Auswahl + ARIA + Keyboard (Roving Tabindex)
+ function selectSwatch(target) {
+ panel.querySelectorAll('.note-color-swatch').forEach((s) => {
+ s.classList.remove('note-color-swatch--active');
+ s.setAttribute('aria-checked', 'false');
+ s.setAttribute('tabindex', '-1');
+ });
+ target.classList.add('note-color-swatch--active');
+ target.setAttribute('aria-checked', 'true');
+ target.setAttribute('tabindex', '0');
+ }
panel.querySelectorAll('.note-color-swatch').forEach((sw) => {
- sw.addEventListener('click', () => {
- panel.querySelectorAll('.note-color-swatch').forEach((s) => s.classList.remove('note-color-swatch--active'));
- sw.classList.add('note-color-swatch--active');
+ sw.addEventListener('click', () => { selectSwatch(sw); sw.focus(); });
+ sw.addEventListener('keydown', (e) => {
+ const swatches = [...panel.querySelectorAll('.note-color-swatch')];
+ const idx = swatches.indexOf(sw);
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ const next = swatches[(idx + 1) % swatches.length];
+ selectSwatch(next); next.focus();
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ const prev = swatches[(idx - 1 + swatches.length) % swatches.length];
+ selectSwatch(prev); prev.focus();
+ } else if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ selectSwatch(sw);
+ }
});
});
diff --git a/public/pages/tasks.js b/public/pages/tasks.js
index bbd079f..dd8abe7 100644
--- a/public/pages/tasks.js
+++ b/public/pages/tasks.js
@@ -1092,6 +1092,13 @@ function updateOverdueBadge() {
}).length;
document.querySelectorAll('[data-route="/tasks"] .nav-badge').forEach((el) => el.remove());
+ document.querySelectorAll('[data-route="/tasks"]').forEach((navItem) => {
+ const baseLabel = t('tasks.title');
+ navItem.setAttribute('aria-label', overdue > 0
+ ? t('tasks.navLabelOverdue', { count: overdue })
+ : baseLabel
+ );
+ });
if (overdue > 0) {
document.querySelectorAll('[data-route="/tasks"]').forEach((navItem) => {
let anchor = navItem.querySelector('.nav-item__icon-wrap');
@@ -1108,6 +1115,7 @@ function updateOverdueBadge() {
}
const badge = document.createElement('span');
badge.className = 'nav-badge';
+ badge.setAttribute('aria-hidden', 'true');
badge.textContent = String(overdue);
anchor.appendChild(badge);
});
diff --git a/public/reminders.js b/public/reminders.js
index 660491b..72e5c26 100644
--- a/public/reminders.js
+++ b/public/reminders.js
@@ -69,8 +69,15 @@ function showBrowserNotification(title, body) {
* @param {number} count
*/
function updateBellBadge(count) {
+ const navLabel = count > 0
+ ? t(count === 1 ? 'reminders.pendingBadgeTitle' : 'reminders.pendingBadgeTitlePlural', { count })
+ : t('nav.reminders');
+ document.querySelectorAll('[data-route="/reminders"]').forEach((navItem) => {
+ navItem.setAttribute('aria-label', navLabel);
+ });
document.querySelectorAll('.reminder-bell-badge').forEach((badge) => {
if (count > 0) {
+ badge.setAttribute('aria-hidden', 'true');
badge.textContent = count > 9 ? '9+' : String(count);
badge.hidden = false;
} else {
diff --git a/public/router.js b/public/router.js
index 60fa990..f07aabd 100644
--- a/public/router.js
+++ b/public/router.js
@@ -267,6 +267,14 @@ async function renderPage(route, previousPath = null) {
await module.render(pageWrapper, { user: currentUser });
+ // Route-Announcer: Screenreader über Seitenwechsel informieren (gezielt, nicht gesamter Inhalt)
+ const announcer = document.getElementById('route-announcer');
+ if (announcer) {
+ const pageLabel = navItems().find((n) => n.path === path)?.label ?? path;
+ announcer.textContent = '';
+ setTimeout(() => { announcer.textContent = pageLabel; }, 50);
+ }
+
// Erst nach render() + CSS sichtbar machen und Animation starten
pageWrapper.style.opacity = '';
pageWrapper.classList.add(inClass);
@@ -356,7 +364,6 @@ function renderAppShell(container) {
const main = document.createElement('main');
main.className = 'app-content';
main.id = 'main-content';
- main.setAttribute('aria-live', 'polite');
const bottomNav = document.createElement('nav');
bottomNav.className = 'nav-bottom';
@@ -441,7 +448,13 @@ function renderAppShell(container) {
toastContainer.id = 'toast-container';
toastContainer.setAttribute('aria-live', 'assertive');
- container.replaceChildren(skipLink, sidebar, main, bottomNav, backdrop, moreSheet, searchOverlay, toastContainer);
+ const routeAnnouncer = document.createElement('div');
+ routeAnnouncer.id = 'route-announcer';
+ routeAnnouncer.className = 'sr-only';
+ routeAnnouncer.setAttribute('aria-live', 'polite');
+ routeAnnouncer.setAttribute('aria-atomic', 'true');
+
+ container.replaceChildren(skipLink, sidebar, main, bottomNav, backdrop, moreSheet, searchOverlay, toastContainer, routeAnnouncer);
// Klick-Handler für alle Nav-Links
container.querySelectorAll('[data-route]').forEach((el) => {