fix: UX & accessibility improvements (7 fixes)
- Add --target-base token (44px) for search close button - Correct touch targets: btn--icon-sm 36→44px min-size - Remove role=presentation from modal overlay - Fix modal swipe-to-close stuck state on upswing - Add focus trap to search overlay - Generate unique SVG gradient IDs to prevent collisions - Add skip-to-content link for keyboard/screen reader users
This commit is contained in:
@@ -131,7 +131,7 @@ function _wireSheetSwipe(panel) {
|
|||||||
panel.addEventListener('touchmove', (e) => {
|
panel.addEventListener('touchmove', (e) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
const dy = e.touches[0].clientY - startY;
|
const dy = e.touches[0].clientY - startY;
|
||||||
if (dy < 0) { dragging = false; return; } // Aufwärts-Scroll: Swipe abbrechen
|
if (dy < 0) { panel.style.transform = 'translateY(0)'; return; } // Aufwärts: Panel zurücksetzen, dragging bleibt aktiv
|
||||||
// Erst ab 10px Bewegung animieren: Verhindert winzige Transforms durch
|
// Erst ab 10px Bewegung animieren: Verhindert winzige Transforms durch
|
||||||
// normale Taps, die danach zurückgesetzt werden müssten.
|
// normale Taps, die danach zurückgesetzt werden müssten.
|
||||||
if (dy > 10) panel.style.transform = `translateY(${(dy - 10) * 0.6}px)`;
|
if (dy > 10) panel.style.transform = `translateY(${(dy - 10) * 0.6}px)`;
|
||||||
@@ -218,7 +218,7 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}
|
|||||||
const sizeClass = size !== 'md' ? ` modal-panel--${size}` : '';
|
const sizeClass = size !== 'md' ? ` modal-panel--${size}` : '';
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="modal-overlay" id="shared-modal-overlay" aria-label="${t('modal.overlayLabel')}" role="presentation">
|
<div class="modal-overlay" id="shared-modal-overlay" aria-label="${t('modal.overlayLabel')}">
|
||||||
<div class="modal-panel${sizeClass}" role="dialog" aria-modal="true"
|
<div class="modal-panel${sizeClass}" role="dialog" aria-modal="true"
|
||||||
aria-labelledby="shared-modal-title">
|
aria-labelledby="shared-modal-title">
|
||||||
<div class="modal-panel__header">
|
<div class="modal-panel__header">
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
<script src="/lucide.min.js"></script>
|
<script src="/lucide.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Skip-Link: sichtbar bei Keyboard-Fokus, verknüpft mit <main id="main-content"> -->
|
||||||
|
<a href="#main-content" class="sr-only">Zum Hauptinhalt springen</a>
|
||||||
|
|
||||||
<!-- App-Shell - wird durch JavaScript gefüllt -->
|
<!-- App-Shell - wird durch JavaScript gefüllt -->
|
||||||
<div id="app" class="app-shell">
|
<div id="app" class="app-shell">
|
||||||
<!-- Skeleton-Loading während Initialisierung -->
|
<!-- Skeleton-Loading während Initialisierung -->
|
||||||
|
|||||||
+27
-2
@@ -313,7 +313,8 @@ function renderAppShell(container) {
|
|||||||
logoSvg.setAttribute('fill', 'none');
|
logoSvg.setAttribute('fill', 'none');
|
||||||
const defs = document.createElementNS(SVG_NS, 'defs');
|
const defs = document.createElementNS(SVG_NS, 'defs');
|
||||||
const grad = document.createElementNS(SVG_NS, 'linearGradient');
|
const grad = document.createElementNS(SVG_NS, 'linearGradient');
|
||||||
grad.setAttribute('id', 'oikos-logo-bg');
|
const gradId = `oikos-logo-bg-${Math.random().toString(36).slice(2, 7)}`;
|
||||||
|
grad.setAttribute('id', gradId);
|
||||||
grad.setAttribute('x1', '0'); grad.setAttribute('y1', '0');
|
grad.setAttribute('x1', '0'); grad.setAttribute('y1', '0');
|
||||||
grad.setAttribute('x2', '160'); grad.setAttribute('y2', '160');
|
grad.setAttribute('x2', '160'); grad.setAttribute('y2', '160');
|
||||||
grad.setAttribute('gradientUnits', 'userSpaceOnUse');
|
grad.setAttribute('gradientUnits', 'userSpaceOnUse');
|
||||||
@@ -328,7 +329,7 @@ function renderAppShell(container) {
|
|||||||
logoSvg.appendChild(defs);
|
logoSvg.appendChild(defs);
|
||||||
const bgRect = document.createElementNS(SVG_NS, 'rect');
|
const bgRect = document.createElementNS(SVG_NS, 'rect');
|
||||||
bgRect.setAttribute('width', '160'); bgRect.setAttribute('height', '160');
|
bgRect.setAttribute('width', '160'); bgRect.setAttribute('height', '160');
|
||||||
bgRect.setAttribute('rx', '36'); bgRect.setAttribute('fill', 'url(#oikos-logo-bg)');
|
bgRect.setAttribute('rx', '36'); bgRect.setAttribute('fill', `url(#${gradId})`);
|
||||||
logoSvg.appendChild(bgRect);
|
logoSvg.appendChild(bgRect);
|
||||||
const housePath = document.createElementNS(SVG_NS, 'path');
|
const housePath = document.createElementNS(SVG_NS, 'path');
|
||||||
housePath.setAttribute('d', 'M80 36L36 72V120C36 122.2 37.8 124 40 124H68V96H92V124H120C122.2 124 124 122.2 124 120V72L80 36Z');
|
housePath.setAttribute('d', 'M80 36L36 72V120C36 122.2 37.8 124 40 124H68V96H92V124H120C122.2 124 124 122.2 124 120V72L80 36Z');
|
||||||
@@ -528,17 +529,41 @@ function initSearch(container) {
|
|||||||
const results = container.querySelector('#search-results');
|
const results = container.querySelector('#search-results');
|
||||||
if (!searchBtn || !overlay || !input || !results) return;
|
if (!searchBtn || !overlay || !input || !results) return;
|
||||||
|
|
||||||
|
// Leichtgewichtiger Focus Trap für das Search Overlay.
|
||||||
|
// Eigenständig (kein modal.js), da modul-globale Variablen in modal.js
|
||||||
|
// bei gleichzeitig offenem Modal überschrieben würden.
|
||||||
|
let _searchTrapHandler = null;
|
||||||
|
|
||||||
function openSearch() {
|
function openSearch() {
|
||||||
if (window._closeMoreSheet) window._closeMoreSheet();
|
if (window._closeMoreSheet) window._closeMoreSheet();
|
||||||
overlay.setAttribute('aria-hidden', 'false');
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
overlay.classList.add('search-overlay--visible');
|
overlay.classList.add('search-overlay--visible');
|
||||||
setTimeout(() => input.focus(), 50);
|
setTimeout(() => input.focus(), 50);
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
|
||||||
|
const FOCUSABLE = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])';
|
||||||
|
_searchTrapHandler = (e) => {
|
||||||
|
if (e.key !== 'Tab') return;
|
||||||
|
const focusable = Array.from(overlay.querySelectorAll(FOCUSABLE));
|
||||||
|
if (!focusable.length) return;
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault(); last.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault(); first.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
overlay.addEventListener('keydown', _searchTrapHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSearch() {
|
function closeSearch() {
|
||||||
overlay.setAttribute('aria-hidden', 'true');
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
overlay.classList.remove('search-overlay--visible');
|
overlay.classList.remove('search-overlay--visible');
|
||||||
|
if (_searchTrapHandler) {
|
||||||
|
overlay.removeEventListener('keydown', _searchTrapHandler);
|
||||||
|
_searchTrapHandler = null;
|
||||||
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
results.replaceChildren();
|
results.replaceChildren();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1990,13 +1990,11 @@
|
|||||||
button i[data-lucide],
|
button i[data-lucide],
|
||||||
button svg { pointer-events: none; }
|
button svg { pointer-events: none; }
|
||||||
|
|
||||||
/* Kompakter Icon-Button (36×36) für Icons in engen Listenkontexten */
|
/* Kompakter Icon-Button: 44px Klickfläche, optisch kompakt durch geringes Padding */
|
||||||
.btn--icon-sm {
|
.btn--icon-sm {
|
||||||
padding: var(--space-1);
|
padding: var(--space-1);
|
||||||
min-height: unset;
|
min-height: var(--target-base);
|
||||||
min-width: unset;
|
min-width: var(--target-base);
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Textarea: vertikale Größenänderung ist nutzbar */
|
/* Textarea: vertikale Größenänderung ist nutzbar */
|
||||||
|
|||||||
@@ -307,9 +307,10 @@
|
|||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* 11b. Touch-Target Sizes
|
* 11b. Touch-Target Sizes
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
--target-sm: 32px;
|
--target-sm: 32px; /* Visuelle Größe (z.B. Logos) — kein Touch-Target */
|
||||||
--target-md: 40px;
|
--target-md: 40px; /* Desktop Touch-Target (Maus) */
|
||||||
--target-lg: 48px;
|
--target-lg: 48px; /* Mobile Touch-Target (Finger) */
|
||||||
|
--target-base: 44px; /* iOS-Minimum Touch-Target (44pt) */
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* 12. Layout
|
* 12. Layout
|
||||||
|
|||||||
Reference in New Issue
Block a user