fix(modal): fix sheet swipe tap-cancel and modal close race condition on iOS
Sheet swipe: add 10 px dead zone before transform, reset via rAF in touchend so iOS WebKit does not cancel subsequent click events on child buttons. Modal close: capture active overlay before animation to prevent race where _doClose() removes the new modal instead of the old one when a confirm dialog opens immediately after another modal closes (delete button not responding).
This commit is contained in:
+46
-22
@@ -132,16 +132,23 @@ function _wireSheetSwipe(panel) {
|
|||||||
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) { dragging = false; return; } // Aufwärts-Scroll: Swipe abbrechen
|
||||||
panel.style.transform = `translateY(${dy * 0.6}px)`;
|
// 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)`;
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
panel.addEventListener('touchend', (e) => {
|
panel.addEventListener('touchend', (e) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
dragging = false;
|
dragging = false;
|
||||||
const dy = e.changedTouches[0].clientY - startY;
|
const dy = e.changedTouches[0].clientY - startY;
|
||||||
panel.style.transform = '';
|
|
||||||
if (dy > 80) {
|
if (dy > 80) {
|
||||||
|
panel.style.transform = '';
|
||||||
closeModal();
|
closeModal();
|
||||||
|
} else {
|
||||||
|
// Transform-Reset per rAF verzögern: DOM-Mutationen direkt in touchend
|
||||||
|
// unterbrechen auf iOS WebKit die Touch→Click-Konvertierung – der click-Event
|
||||||
|
// auf Child-Elementen (Buttons) wird gecancelt → Buttons reagieren nicht.
|
||||||
|
requestAnimationFrame(() => { panel.style.transform = ''; });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -150,23 +157,30 @@ function _wireSheetSwipe(panel) {
|
|||||||
// _doClose - gemeinsame Cleanup-Logik
|
// _doClose - gemeinsame Cleanup-Logik
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function _doClose() {
|
function _doClose(overlayEl) {
|
||||||
if (!activeOverlay) return;
|
const target = overlayEl ?? activeOverlay;
|
||||||
activeOverlay.remove();
|
if (!target) return;
|
||||||
activeOverlay = null;
|
|
||||||
|
|
||||||
// Scroll-Lock aufheben
|
target.remove();
|
||||||
document.body.style.overflow = '';
|
|
||||||
|
|
||||||
// Focus-Restore
|
// Globalen State nur zurücksetzen wenn kein neues Modal zwischenzeitlich geöffnet wurde.
|
||||||
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
// (activeOverlay !== target bedeutet: openModal hat bereits ein neues Modal registriert)
|
||||||
previouslyFocused.focus();
|
if (activeOverlay === target) {
|
||||||
previouslyFocused = null;
|
activeOverlay = null;
|
||||||
}
|
|
||||||
|
|
||||||
// Standalone: Statusbar-Farbe zur aktuellen Route wiederherstellen
|
// Scroll-Lock aufheben
|
||||||
if (window.oikos?.restoreThemeColor) {
|
document.body.style.overflow = '';
|
||||||
window.oikos.restoreThemeColor();
|
|
||||||
|
// Focus-Restore
|
||||||
|
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
previouslyFocused = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone: Statusbar-Farbe zur aktuellen Route wiederherstellen
|
||||||
|
if (window.oikos?.restoreThemeColor) {
|
||||||
|
window.oikos.restoreThemeColor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +200,14 @@ function _doClose() {
|
|||||||
* @param {string} [opts.size='md'] - 'sm' | 'md' | 'lg'
|
* @param {string} [opts.size='md'] - 'sm' | 'md' | 'lg'
|
||||||
*/
|
*/
|
||||||
export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) {
|
export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) {
|
||||||
// Vorheriges Modal schließen (kein Stacking)
|
// Vorheriges Modal schließen (kein Stacking).
|
||||||
if (activeOverlay) closeModal();
|
// ID sofort entfernen damit getElementById() nach dem Einfügen des neuen Modals
|
||||||
|
// nicht die noch animierende alte Instanz zurückgibt – sonst landen alle
|
||||||
|
// Event-Listener am falschen Element und Buttons reagieren nicht.
|
||||||
|
if (activeOverlay) {
|
||||||
|
activeOverlay.removeAttribute('id');
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
// Focus-Restore vorbereiten
|
// Focus-Restore vorbereiten
|
||||||
previouslyFocused = document.activeElement;
|
previouslyFocused = document.activeElement;
|
||||||
@@ -264,7 +284,11 @@ export function closeModal() {
|
|||||||
|
|
||||||
document.removeEventListener('keydown', onEscape);
|
document.removeEventListener('keydown', onEscape);
|
||||||
|
|
||||||
const panel = activeOverlay.querySelector('.modal-panel');
|
// Overlay sofort sichern: Bei Mobile-Animation öffnet openModal() ein neues Modal
|
||||||
|
// bevor animationend feuert. Ohne capturedOverlay würde _doClose() das neue Modal
|
||||||
|
// statt des alten entfernen (Race Condition → Buttons im Confirm-Dialog reagieren nicht).
|
||||||
|
const capturedOverlay = activeOverlay;
|
||||||
|
const panel = capturedOverlay.querySelector('.modal-panel');
|
||||||
|
|
||||||
// Focus-Trap-Handler und Virtual-Keyboard-Listener entfernen
|
// Focus-Trap-Handler und Virtual-Keyboard-Listener entfernen
|
||||||
if (focusTrapHandler) {
|
if (focusTrapHandler) {
|
||||||
@@ -280,15 +304,15 @@ export function closeModal() {
|
|||||||
if (isMobile && panel) {
|
if (isMobile && panel) {
|
||||||
panel.classList.add('modal-panel--closing');
|
panel.classList.add('modal-panel--closing');
|
||||||
// Fallback-Timer falls animationend nicht feuert (prefers-reduced-motion, Tab-Wechsel etc.)
|
// Fallback-Timer falls animationend nicht feuert (prefers-reduced-motion, Tab-Wechsel etc.)
|
||||||
const fallback = setTimeout(_doClose, 300);
|
const fallback = setTimeout(() => _doClose(capturedOverlay), 300);
|
||||||
panel.addEventListener('animationend', () => {
|
panel.addEventListener('animationend', () => {
|
||||||
clearTimeout(fallback);
|
clearTimeout(fallback);
|
||||||
_doClose();
|
_doClose(capturedOverlay);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_doClose();
|
_doClose(capturedOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user