fix: improve modal behavior on web and Android scrolling
- Restructure ModalBase to use absolute-positioned backdrop behind card content, fixing modal stacking issues on web (React Native Web portals) - Hide EventOverlay when DeleteEventModal is open to prevent z-index conflicts on web - Add nestedScrollEnabled to CardBase ScrollView for Android - Use TouchableOpacity with delayPressIn in EventCard for scroll-friendly touch handling - Keep eventToDelete state stable during modal fade-out to prevent content flash between recurring/single variants - Fix German umlauts in DeleteEventModal
This commit is contained in:
12
CLAUDE.md
12
CLAUDE.md
@@ -200,6 +200,11 @@ Reusable base components for cards and modals with consistent styling.
|
|||||||
|
|
||||||
ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support.
|
ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support.
|
||||||
|
|
||||||
|
**ModalBase Architecture Note:** Uses absolute-positioned backdrop behind the card content (not nested Pressables). This approach:
|
||||||
|
- Fixes modal stacking issues on web (React Native Web renders modals as DOM portals)
|
||||||
|
- Allows proper scrolling on Android (no touch event conflicts)
|
||||||
|
- Card naturally blocks touches from reaching backdrop due to z-order
|
||||||
|
|
||||||
**Component Hierarchy:**
|
**Component Hierarchy:**
|
||||||
```
|
```
|
||||||
CardBase
|
CardBase
|
||||||
@@ -502,6 +507,7 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
|||||||
- Supports events from adjacent months visible in grid
|
- Supports events from adjacent months visible in grid
|
||||||
- Uses `useFocusEffect` for automatic reload on tab focus
|
- Uses `useFocusEffect` for automatic reload on tab focus
|
||||||
- DeleteEventModal integration for recurring event deletion with three modes
|
- DeleteEventModal integration for recurring event deletion with three modes
|
||||||
|
- EventOverlay hides when DeleteEventModal is open (fixes modal stacking on web)
|
||||||
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
|
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
|
||||||
- **Multiple event proposals**: AI can propose multiple events in one response
|
- **Multiple event proposals**: AI can propose multiple events in one response
|
||||||
- Arrow navigation between proposals with "Event X von Y" counter
|
- Arrow navigation between proposals with "Event X von Y" counter
|
||||||
@@ -515,10 +521,10 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
|||||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes
|
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes
|
||||||
- `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination and recurring delete support
|
- `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination and recurring delete support
|
||||||
- `CardBase`: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props
|
- `CardBase`: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props, ScrollView uses `nestedScrollEnabled` for Android
|
||||||
- `ModalBase`: Reusable modal wrapper with backdrop, uses CardBase internally - provides click-outside-to-close and Android back button support
|
- `ModalBase`: Reusable modal wrapper with backdrop (absolute-positioned behind card), uses CardBase internally - provides click-outside-to-close, Android back button support, and proper scrolling on Android
|
||||||
- `EventCardBase`: Event card with date/time/recurring icons - uses CardBase for structure
|
- `EventCardBase`: Event card with date/time/recurring icons - uses CardBase for structure
|
||||||
- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display
|
- `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling)
|
||||||
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions with deleteMode display)
|
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions with deleteMode display)
|
||||||
- `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring
|
- `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring
|
||||||
- `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day
|
- `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day
|
||||||
|
|||||||
@@ -188,14 +188,14 @@ const Calendar = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete event:", error);
|
console.error("Failed to delete event:", error);
|
||||||
} finally {
|
|
||||||
setEventToDelete(null);
|
|
||||||
}
|
}
|
||||||
|
// Note: Don't clear eventToDelete here - it will be overwritten when opening a new modal.
|
||||||
|
// Clearing it during fade-out animation causes the modal content to flash from recurring to single.
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCancel = () => {
|
const handleDeleteCancel = () => {
|
||||||
setDeleteModalVisible(false);
|
setDeleteModalVisible(false);
|
||||||
setEventToDelete(null);
|
// Note: Don't clear eventToDelete - keeps modal content stable during fade-out animation
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get events for selected date
|
// Get events for selected date
|
||||||
@@ -222,7 +222,7 @@ const Calendar = () => {
|
|||||||
onDayPress={handleDayPress}
|
onDayPress={handleDayPress}
|
||||||
/>
|
/>
|
||||||
<EventOverlay
|
<EventOverlay
|
||||||
visible={selectedDate !== null}
|
visible={selectedDate !== null && !deleteModalVisible}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
events={selectedDateEvents}
|
events={selectedDateEvents}
|
||||||
onClose={handleCloseOverlay}
|
onClose={handleCloseOverlay}
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ export const CardBase = ({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{scrollable ? (
|
{scrollable ? (
|
||||||
<ScrollView style={{ maxHeight: maxContentHeight }}>
|
<ScrollView
|
||||||
|
style={{ maxHeight: maxContentHeight }}
|
||||||
|
nestedScrollEnabled={true}
|
||||||
|
>
|
||||||
{contentElement}
|
{contentElement}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const DeleteEventModal = ({
|
|||||||
// Non-recurring event: simple confirmation
|
// Non-recurring event: simple confirmation
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-base mb-4" style={{ color: theme.textPrimary }}>
|
<Text className="text-base mb-4" style={{ color: theme.textPrimary }}>
|
||||||
Moechtest du diesen Termin wirklich loeschen?
|
Möchtest du diesen Termin wirklich löschen?
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => onConfirm("all")}
|
onPress={() => onConfirm("all")}
|
||||||
@@ -90,7 +90,7 @@ export const DeleteEventModal = ({
|
|||||||
className="font-medium text-base text-center"
|
className="font-medium text-base text-center"
|
||||||
style={{ color: theme.buttonText }}
|
style={{ color: theme.buttonText }}
|
||||||
>
|
>
|
||||||
Loeschen
|
Löschen
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { View, Pressable } from "react-native";
|
import { View, TouchableOpacity } from "react-native";
|
||||||
import { ExpandedEvent } from "@calchat/shared";
|
import { ExpandedEvent } from "@calchat/shared";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { useThemeStore } from "../stores/ThemeStore";
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
@@ -21,10 +21,12 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
description={event.description}
|
description={event.description}
|
||||||
isRecurring={event.isRecurring}
|
isRecurring={event.isRecurring}
|
||||||
>
|
>
|
||||||
{/* Action buttons */}
|
{/* Action buttons - TouchableOpacity with delayPressIn allows ScrollView to detect scroll gestures */}
|
||||||
<View className="flex-row justify-end mt-3 gap-3">
|
<View className="flex-row justify-end mt-3 gap-3">
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={onEdit}
|
onPress={onEdit}
|
||||||
|
delayPressIn={100}
|
||||||
|
activeOpacity={0.7}
|
||||||
className="w-10 h-10 rounded-full items-center justify-center"
|
className="w-10 h-10 rounded-full items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -32,9 +34,11 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Feather name="edit-2" size={18} color={theme.textPrimary} />
|
<Feather name="edit-2" size={18} color={theme.textPrimary} />
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={onDelete}
|
onPress={onDelete}
|
||||||
|
delayPressIn={100}
|
||||||
|
activeOpacity={0.7}
|
||||||
className="w-10 h-10 rounded-full items-center justify-center"
|
className="w-10 h-10 rounded-full items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -42,7 +46,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Feather name="trash-2" size={18} color={theme.textPrimary} />
|
<Feather name="trash-2" size={18} color={theme.textPrimary} />
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</EventCardBase>
|
</EventCardBase>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Modal, Pressable } from "react-native";
|
import { Modal, Pressable, View } from "react-native";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useThemeStore } from "../stores/ThemeStore";
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
import { CardBase } from "./CardBase";
|
import { CardBase } from "./CardBase";
|
||||||
@@ -36,19 +36,21 @@ export const ModalBase = ({
|
|||||||
animationType="fade"
|
animationType="fade"
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
|
<View className="flex-1 justify-center items-center">
|
||||||
|
{/* Backdrop - absolute positioned behind the card */}
|
||||||
<Pressable
|
<Pressable
|
||||||
className="flex-1 justify-center items-center"
|
className="absolute inset-0"
|
||||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
>
|
/>
|
||||||
<Pressable
|
{/* Card content - on top, naturally blocks touches to backdrop */}
|
||||||
|
<View
|
||||||
className="w-11/12 rounded-2xl overflow-hidden"
|
className="w-11/12 rounded-2xl overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: theme.primeBg,
|
backgroundColor: theme.primeBg,
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
borderColor: theme.borderPrimary,
|
borderColor: theme.borderPrimary,
|
||||||
}}
|
}}
|
||||||
onPress={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<CardBase
|
<CardBase
|
||||||
title={title}
|
title={title}
|
||||||
@@ -65,8 +67,8 @@ export const ModalBase = ({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CardBase>
|
</CardBase>
|
||||||
</Pressable>
|
</View>
|
||||||
</Pressable>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user