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) => {
|
||||
if (!dragging) return;
|
||||
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
|
||||
// normale Taps, die danach zurückgesetzt werden müssten.
|
||||
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 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"
|
||||
aria-labelledby="shared-modal-title">
|
||||
<div class="modal-panel__header">
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
<script src="/lucide.min.js"></script>
|
||||
</head>
|
||||
<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 -->
|
||||
<div id="app" class="app-shell">
|
||||
<!-- Skeleton-Loading während Initialisierung -->
|
||||
|
||||
+27
-2
@@ -313,7 +313,8 @@ function renderAppShell(container) {
|
||||
logoSvg.setAttribute('fill', 'none');
|
||||
const defs = document.createElementNS(SVG_NS, 'defs');
|
||||
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('x2', '160'); grad.setAttribute('y2', '160');
|
||||
grad.setAttribute('gradientUnits', 'userSpaceOnUse');
|
||||
@@ -328,7 +329,7 @@ function renderAppShell(container) {
|
||||
logoSvg.appendChild(defs);
|
||||
const bgRect = document.createElementNS(SVG_NS, 'rect');
|
||||
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);
|
||||
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');
|
||||
@@ -528,17 +529,41 @@ function initSearch(container) {
|
||||
const results = container.querySelector('#search-results');
|
||||
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() {
|
||||
if (window._closeMoreSheet) window._closeMoreSheet();
|
||||
overlay.setAttribute('aria-hidden', 'false');
|
||||
overlay.classList.add('search-overlay--visible');
|
||||
setTimeout(() => input.focus(), 50);
|
||||
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() {
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
overlay.classList.remove('search-overlay--visible');
|
||||
if (_searchTrapHandler) {
|
||||
overlay.removeEventListener('keydown', _searchTrapHandler);
|
||||
_searchTrapHandler = null;
|
||||
}
|
||||
input.value = '';
|
||||
results.replaceChildren();
|
||||
}
|
||||
|
||||
@@ -1990,13 +1990,11 @@
|
||||
button i[data-lucide],
|
||||
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 {
|
||||
padding: var(--space-1);
|
||||
min-height: unset;
|
||||
min-width: unset;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-height: var(--target-base);
|
||||
min-width: var(--target-base);
|
||||
}
|
||||
|
||||
/* Textarea: vertikale Größenänderung ist nutzbar */
|
||||
|
||||
@@ -307,9 +307,10 @@
|
||||
/* --------------------------------------------------------
|
||||
* 11b. Touch-Target Sizes
|
||||
* -------------------------------------------------------- */
|
||||
--target-sm: 32px;
|
||||
--target-md: 40px;
|
||||
--target-lg: 48px;
|
||||
--target-sm: 32px; /* Visuelle Größe (z.B. Logos) — kein Touch-Target */
|
||||
--target-md: 40px; /* Desktop Touch-Target (Maus) */
|
||||
--target-lg: 48px; /* Mobile Touch-Target (Finger) */
|
||||
--target-base: 44px; /* iOS-Minimum Touch-Target (44pt) */
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 12. Layout
|
||||
|
||||
Reference in New Issue
Block a user