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
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:
parent
c9175bd2fd
commit
295c7e7efc
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
56
frontend/src/components/FormModalOverlay.jsx
Normal file
56
frontend/src/components/FormModalOverlay.jsx
Normal 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
60
frontend/src/hooks/useModalScrollLock.js
Normal file
60
frontend/src/hooks/useModalScrollLock.js
Normal 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])
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user