Refactor modal components to use FormModalOverlay for improved consistency and functionality
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m9s

- Replaced div elements with FormModalOverlay in SaveExercisesAsModuleModal, TrainingPlanningUnitFormModal, and TrainingPublishToFrameworkModal for a unified modal structure.
- Enhanced modal styling and behavior, including adjustments for responsiveness and accessibility.
- Introduced new CSS rules to manage overflow and scrolling behavior when modals are active, improving user experience across devices.
This commit is contained in:
Lars 2026-05-19 10:47:44 +02:00
parent c9175bd2fd
commit 295c7e7efc
6 changed files with 173 additions and 6 deletions

View File

@ -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));
}

View File

@ -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(
<div
ref={overlayRef}
tabIndex={-1}
data-testid={testId}
className={overlayClass}
onClick={
onBackdropClick
? (e) => {
if (e.target === e.currentTarget) onBackdropClick()
}
: undefined
}
>
{children}
</div>,
document.body,
)
}

View File

@ -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 (
<div className="modal-overlay modal-overlay--form modal-overlay--raised">
<FormModalOverlay open={open} raised onBackdropClick={onClose}>
<div className="card modal-panel--form modal-panel--narrow">
<h2 className="modal-panel__title">Übungen als Trainingsmodul</h2>
<p className="modal-panel__intro">
@ -306,6 +307,6 @@ export default function SaveExercisesAsModuleModal({
</div>
) : null}
</div>
</div>
</FormModalOverlay>
)
}

View File

@ -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 (
<div data-testid="planning-unit-form-modal" className="modal-overlay modal-overlay--form">
<FormModalOverlay open={open} data-testid="planning-unit-form-modal" onBackdropClick={onCancel}>
<div className="modal-panel--form">
<h2 className="modal-panel__title">
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
@ -575,6 +576,6 @@ export default function TrainingPlanningUnitFormModal({
/>
</form>
</div>
</div>
</FormModalOverlay>
)
}

View File

@ -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 (
<div className="modal-overlay modal-overlay--form modal-overlay--raised">
<FormModalOverlay open={open} raised onBackdropClick={resetAndClose}>
<div className="card modal-panel--form modal-panel--narrow">
<h2 className="modal-panel__title">Ablauf ins Rahmenprogramm übernehmen</h2>
<p className="modal-panel__intro">
@ -400,6 +401,6 @@ export default function TrainingPublishToFrameworkModal({
/>
</form>
</div>
</div>
</FormModalOverlay>
)
}

View File

@ -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])
}