fix: resolve modal header scroll-behind issue on iOS PWA (closes #50)
position: sticky on .modal-panel__header failed on iOS WebKit when the scroll container had padding-top applied (drag-handle spacing). Restructured modal layout: .modal-panel is now a flex-column with overflow:hidden and .modal-panel__body handles scrolling (overflow-y:auto, flex:1). The header is a non-scrolled flex sibling, so it stays visible without sticky. Updated swipe-to-close to read .modal-panel__body scroll position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.20.11] - 2026-04-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- PWA: modal header (task / calendar event) no longer scrolls out of view when the form content exceeds the modal height. Root cause: `position: sticky` on `.modal-panel__header` fails on iOS WebKit when the scroll container (`.modal-panel`) has `padding-top` applied (a known WebKit quirk). Fixed by restructuring the modal layout: `.modal-panel` is now a `flex-column` container with `overflow: hidden`, and scrolling is handled by `.modal-panel__body` (`overflow-y: auto; flex: 1`). The header is always visible as a non-scrolled flex sibling. Swipe-to-close updated to read scroll position from `.modal-panel__body` instead of `.modal-panel` (closes #50)
|
||||||
|
|
||||||
## [0.20.10] - 2026-04-18
|
## [0.20.10] - 2026-04-18
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.20.10",
|
"version": "0.20.11",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -114,12 +114,15 @@ function _wireSheetSwipe(panel) {
|
|||||||
let startY = 0;
|
let startY = 0;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
|
||||||
|
// Scroll position is now on the body, not the panel itself
|
||||||
|
const scrollBody = panel.querySelector('.modal-panel__body');
|
||||||
|
|
||||||
panel.addEventListener('touchstart', (e) => {
|
panel.addEventListener('touchstart', (e) => {
|
||||||
// Nur von der Handle-Zone (obere 48px) oder wenn Panel ganz oben → Swipe erlauben
|
// Nur von der Handle-Zone (obere 48px) oder wenn Panel ganz oben → Swipe erlauben
|
||||||
const touchY = e.touches[0].clientY;
|
const touchY = e.touches[0].clientY;
|
||||||
const rect = panel.getBoundingClientRect();
|
const rect = panel.getBoundingClientRect();
|
||||||
const isHandleZone = touchY - rect.top < 48;
|
const isHandleZone = touchY - rect.top < 48;
|
||||||
const isScrolledToTop = panel.scrollTop <= 0;
|
const isScrolledToTop = (scrollBody ? scrollBody.scrollTop : panel.scrollTop) <= 0;
|
||||||
if (!isHandleZone && !isScrolledToTop) return;
|
if (!isHandleZone && !isScrolledToTop) return;
|
||||||
startY = touchY;
|
startY = touchY;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
|
|||||||
@@ -648,7 +648,9 @@
|
|||||||
background-color: var(--color-surface);
|
background-color: var(--color-surface);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 90dvh;
|
max-height: 90dvh;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||||
border: 1px solid var(--color-border-subtle);
|
border: 1px solid var(--color-border-subtle);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
@@ -673,10 +675,8 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border-bottom: 1px solid var(--color-border-subtle);
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--color-surface);
|
background-color: var(--color-surface);
|
||||||
z-index: 1;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-panel__title {
|
.modal-panel__title {
|
||||||
@@ -706,6 +706,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal-Content-Grids (z. B. 2 Spalten in Formularen) */
|
/* Modal-Content-Grids (z. B. 2 Spalten in Formularen) */
|
||||||
|
|||||||
Reference in New Issue
Block a user