diff --git a/frontend/src/app.css b/frontend/src/app.css
index e19d511..f89aef8 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -1231,21 +1231,52 @@ a.analysis-split__nav-item {
}
/* Form-Modale: Overlay + Panel (Desktop zentriert, Mobile Vollbild) */
+html.modal-scroll-locked {
+ overflow: hidden;
+ overscroll-behavior: none;
+ height: 100%;
+}
+html.modal-scroll-locked body {
+ overflow: hidden !important;
+ overscroll-behavior: none;
+}
+html.modal-scroll-locked .app-shell,
+html.modal-scroll-locked .app-shell__column {
+ overflow: hidden;
+}
+html.modal-scroll-locked .app-main {
+ overflow: hidden !important;
+ overscroll-behavior: none;
+}
+
.modal-overlay--form {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
overflow: hidden;
+ overscroll-behavior: none;
+ touch-action: none;
box-sizing: border-box;
+ width: 100%;
+ max-width: 100vw;
}
.modal-overlay--form.modal-overlay--raised {
z-index: 1100;
}
+.modal-overlay__focus-catcher {
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ opacity: 0;
+ pointer-events: none;
+}
.modal-panel--form {
display: flex;
flex-direction: column;
@@ -1259,6 +1290,10 @@ a.analysis-split__nav-item {
min-height: 0;
min-width: 0;
box-sizing: border-box;
+ touch-action: manipulation;
+ flex: 0 1 auto;
+ margin: auto;
+ align-self: center;
}
.modal-panel--form.modal-panel--narrow {
max-width: min(560px, 100%);
@@ -1295,7 +1330,9 @@ a.analysis-split__nav-item {
min-width: 0;
overflow-x: hidden;
overflow-y: auto;
+ overscroll-behavior: contain;
overscroll-behavior-x: none;
+ touch-action: pan-y;
-webkit-overflow-scrolling: touch;
padding-bottom: 4px;
}
@@ -1309,13 +1346,24 @@ a.analysis-split__nav-item {
.modal-overlay--form {
padding: 0;
align-items: stretch;
+ justify-content: stretch;
+ width: 100vw;
+ max-width: 100vw;
+ left: 0;
+ right: 0;
}
.modal-panel--form {
max-width: 100%;
+ width: 100%;
max-height: 100dvh;
height: 100dvh;
border-radius: 0;
+ margin: 0;
+ flex: 1 1 auto;
+ align-self: stretch;
padding: 12px;
+ padding-left: max(12px, env(safe-area-inset-left, 0px));
+ padding-right: max(12px, env(safe-area-inset-right, 0px));
padding-top: max(12px, env(safe-area-inset-top, 0px));
padding-bottom: max(0px, env(safe-area-inset-bottom, 0px));
}
diff --git a/frontend/src/components/FormModalOverlay.jsx b/frontend/src/components/FormModalOverlay.jsx
new file mode 100644
index 0000000..6365ae6
--- /dev/null
+++ b/frontend/src/components/FormModalOverlay.jsx
@@ -0,0 +1,56 @@
+import React, { useEffect, useRef } from 'react'
+import { createPortal } from 'react-dom'
+import { useModalScrollLock } from '../hooks/useModalScrollLock'
+
+/**
+ * Formular-Modal am document.body (Portal) — volle Viewport-Breite, kein Hintergrund-Scroll.
+ */
+export default function FormModalOverlay({
+ open,
+ children,
+ raised = false,
+ className = '',
+ onBackdropClick,
+ 'data-testid': testId,
+}) {
+ const overlayRef = useRef(null)
+ useModalScrollLock(open)
+
+ useEffect(() => {
+ if (!open) return undefined
+ const t = window.setTimeout(() => {
+ overlayRef.current?.focus({ preventScroll: true })
+ }, 0)
+ return () => clearTimeout(t)
+ }, [open])
+
+ if (!open) return null
+
+ const overlayClass = [
+ 'modal-overlay',
+ 'modal-overlay--form',
+ raised ? 'modal-overlay--raised' : '',
+ className,
+ ]
+ .filter(Boolean)
+ .join(' ')
+
+ return createPortal(
+
{
+ if (e.target === e.currentTarget) onBackdropClick()
+ }
+ : undefined
+ }
+ >
+ {children}
+
,
+ document.body,
+ )
+}
diff --git a/frontend/src/components/planning/SaveExercisesAsModuleModal.jsx b/frontend/src/components/planning/SaveExercisesAsModuleModal.jsx
index a2d58a9..ee2c119 100644
--- a/frontend/src/components/planning/SaveExercisesAsModuleModal.jsx
+++ b/frontend/src/components/planning/SaveExercisesAsModuleModal.jsx
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import FormActionBar from '../FormActionBar'
+import FormModalOverlay from '../FormModalOverlay'
import api from '../../utils/api'
import { useToast } from '../../context/ToastContext'
import { useAuth } from '../../context/AuthContext'
@@ -147,7 +148,7 @@ export default function SaveExercisesAsModuleModal({
if (!open) return null
return (
-
+
Übungen als Trainingsmodul
@@ -306,6 +307,6 @@ export default function SaveExercisesAsModuleModal({
) : null}
-
+
)
}
diff --git a/frontend/src/components/planning/TrainingPlanningUnitFormModal.jsx b/frontend/src/components/planning/TrainingPlanningUnitFormModal.jsx
index b15004a..7622624 100644
--- a/frontend/src/components/planning/TrainingPlanningUnitFormModal.jsx
+++ b/frontend/src/components/planning/TrainingPlanningUnitFormModal.jsx
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import FormActionBar from '../FormActionBar'
+import FormModalOverlay from '../FormModalOverlay'
import TrainingPlanExerciseVisibilityPanel from '../TrainingPlanExerciseVisibilityPanel'
import TrainingUnitSectionsEditor from '../TrainingUnitSectionsEditor'
import { activeClubMemberships } from '../../utils/activeClub'
@@ -57,7 +58,7 @@ export default function TrainingPlanningUnitFormModal({
const formId = 'planning-unit-form'
return (
-
+
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
@@ -575,6 +576,6 @@ export default function TrainingPlanningUnitFormModal({
/>
-
+
)
}
diff --git a/frontend/src/components/planning/TrainingPublishToFrameworkModal.jsx b/frontend/src/components/planning/TrainingPublishToFrameworkModal.jsx
index 3789013..c7714d6 100644
--- a/frontend/src/components/planning/TrainingPublishToFrameworkModal.jsx
+++ b/frontend/src/components/planning/TrainingPublishToFrameworkModal.jsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import FormActionBar from '../FormActionBar'
+import FormModalOverlay from '../FormModalOverlay'
import api from '../../utils/api'
import { useToast } from '../../context/ToastContext'
import { useAuth } from '../../context/AuthContext'
@@ -192,7 +193,7 @@ export default function TrainingPublishToFrameworkModal({
if (!open) return null
return (
-
+
Ablauf ins Rahmenprogramm übernehmen
@@ -400,6 +401,6 @@ export default function TrainingPublishToFrameworkModal({
/>
-
+
)
}
diff --git a/frontend/src/hooks/useModalScrollLock.js b/frontend/src/hooks/useModalScrollLock.js
new file mode 100644
index 0000000..181dbeb
--- /dev/null
+++ b/frontend/src/hooks/useModalScrollLock.js
@@ -0,0 +1,60 @@
+import { useEffect } from 'react'
+
+let lockDepth = 0
+const saved = {
+ appMainScrollTop: 0,
+ bodyScrollTop: 0,
+ appMainOverflow: '',
+ appMainTouchAction: '',
+}
+
+function queryAppMain() {
+ return document.querySelector('.app-main')
+}
+
+function applyLock() {
+ document.documentElement.classList.add('modal-scroll-locked')
+
+ const appMain = queryAppMain()
+ if (appMain) {
+ saved.appMainScrollTop = appMain.scrollTop
+ saved.appMainOverflow = appMain.style.overflow
+ saved.appMainTouchAction = appMain.style.touchAction
+ appMain.style.overflow = 'hidden'
+ appMain.style.touchAction = 'none'
+ }
+
+ saved.bodyScrollTop = window.scrollY || document.documentElement.scrollTop || 0
+ document.body.style.overflow = 'hidden'
+ document.body.style.overscrollBehavior = 'none'
+}
+
+function releaseLock() {
+ document.documentElement.classList.remove('modal-scroll-locked')
+
+ const appMain = queryAppMain()
+ if (appMain) {
+ appMain.style.overflow = saved.appMainOverflow
+ appMain.style.touchAction = saved.appMainTouchAction
+ appMain.scrollTop = saved.appMainScrollTop
+ }
+
+ document.body.style.overflow = ''
+ document.body.style.overscrollBehavior = ''
+ if (saved.bodyScrollTop) {
+ window.scrollTo(0, saved.bodyScrollTop)
+ }
+}
+
+/** Hintergrund-Scroll sperren (.app-main + body), verschachtelte Modals via Zähler. */
+export function useModalScrollLock(active) {
+ useEffect(() => {
+ if (!active) return undefined
+ lockDepth += 1
+ if (lockDepth === 1) applyLock()
+ return () => {
+ lockDepth = Math.max(0, lockDepth - 1)
+ if (lockDepth === 0) releaseLock()
+ }
+ }, [active])
+}