fix: clear the new event slot marker when the editor closes or the tab is left

This commit is contained in:
Linus Rath
2026-10-31 13:29:19 +01:00
parent 826d5ad2be
commit 11fa0365f9
3 changed files with 38 additions and 0 deletions
@@ -89,6 +89,8 @@ interface TimeGridScrollViewProps {
onSelectDate?: (date: Date) => void; onSelectDate?: (date: Date) => void;
onSelectEvent?: (event: CalendarEvent) => void; onSelectEvent?: (event: CalendarEvent) => void;
onCreateAtTime?: (date: Date) => void; onCreateAtTime?: (date: Date) => void;
/** A change takes the "New event" block away (the editor closed, the tab was left). */
draftResetKey?: number;
} }
/** /**
@@ -119,6 +121,7 @@ function TimeGridScrollViewInner({
onSelectDate, onSelectDate,
onSelectEvent, onSelectEvent,
onCreateAtTime, onCreateAtTime,
draftResetKey = 0,
}: TimeGridScrollViewProps) { }: TimeGridScrollViewProps) {
const c = useColors(); const c = useColors();
// "Light event colors" (webmail calendarTintedEvents). // "Light event colors" (webmail calendarTintedEvents).
@@ -314,6 +317,9 @@ function TimeGridScrollViewInner({
// A tap on an empty slot marks it with a "New event" block; tapping that // A tap on an empty slot marks it with a "New event" block; tapping that
// block creates the event. Two taps, so a stray tap never opens the editor. // block creates the event. Two taps, so a stray tap never opens the editor.
const [draft, setDraft] = React.useState<{ key: string; hour: number } | null>(null); const [draft, setDraft] = React.useState<{ key: string; hour: number } | null>(null);
React.useEffect(() => {
setDraft(null);
}, [draftResetKey]);
const handleTapAt = React.useCallback((day: Date, hour: number) => { const handleTapAt = React.useCallback((day: Date, hour: number) => {
const key = dayKey(day); const key = dayKey(day);
setDraft((cur) => (cur && cur.key === key && cur.hour === hour ? null : { key, hour })); setDraft((cur) => (cur && cur.key === key && cur.hour === hour ? null : { key, hour }));
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
// Checked on the source: the suite has no React Native renderer.
const grid = fs.readFileSync(path.resolve(__dirname, '../TimeGridScrollView.tsx'), 'utf8');
const screen = fs.readFileSync(path.resolve(__dirname, '../../../screens/CalendarScreen.tsx'), 'utf8');
describe('the "New event" block a tap on an empty slot leaves', () => {
it('goes when the grid is told to drop it', () => {
expect(grid).toMatch(/React\.useEffect\(\(\) => \{\s*setDraft\(null\);\s*\}, \[draftResetKey\]\);/);
});
it('is dropped when the editor opens or closes and when the user leaves the tab', () => {
const grids = screen.slice(screen.indexOf('<TimeGridScrollView'), screen.indexOf('/>', screen.indexOf('<TimeGridScrollView')));
expect(grids).toMatch(/draftResetKey=\{slotDraftReset\}/);
expect(screen).toMatch(/React\.useEffect\(\(\) => \{\s*setSlotDraftReset\(\(n\) => n \+ 1\);\s*\}, \[modalVisible\]\);/);
// The focus effect's cleanup runs on blur.
expect(screen).toMatch(/useFocusEffect\(\s*React\.useCallback\(\(\) => \(\) => setSlotDraftReset\(\(n\) => n \+ 1\), \[\]\),?\s*\)/);
});
});
+11
View File
@@ -11,6 +11,7 @@ import {
useWindowDimensions, useWindowDimensions,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useFocusEffect } from '@react-navigation/native';
import { TASK_CHIP_ID_PREFIX, multiDayCount } from '../lib/calendar-time-grid'; import { TASK_CHIP_ID_PREFIX, multiDayCount } from '../lib/calendar-time-grid';
import { import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
@@ -239,6 +240,15 @@ export default function CalendarScreen() {
const [modalEvent, setModalEvent] = React.useState<CalendarEvent | null>(null); const [modalEvent, setModalEvent] = React.useState<CalendarEvent | null>(null);
const [modalDate, setModalDate] = React.useState<Date | undefined>(undefined); const [modalDate, setModalDate] = React.useState<Date | undefined>(undefined);
const [modalVisible, setModalVisible] = React.useState(false); const [modalVisible, setModalVisible] = React.useState(false);
// The "New event" block a tap on an empty slot leaves: gone once the
// editor opens or closes, or the user leaves the tab.
const [slotDraftReset, setSlotDraftReset] = React.useState(0);
React.useEffect(() => {
setSlotDraftReset((n) => n + 1);
}, [modalVisible]);
useFocusEffect(
React.useCallback(() => () => setSlotDraftReset((n) => n + 1), []),
);
const [pendingAction, setPendingAction] = React.useState<PendingAction>(null); const [pendingAction, setPendingAction] = React.useState<PendingAction>(null);
const [sidebarVisible, setSidebarVisible] = React.useState(false); const [sidebarVisible, setSidebarVisible] = React.useState(false);
const [tasksVisible, setTasksVisible] = React.useState(false); const [tasksVisible, setTasksVisible] = React.useState(false);
@@ -1177,6 +1187,7 @@ export default function CalendarScreen() {
onSelectDate={viewMode === 'day' ? handleSelectDate : openDay} onSelectDate={viewMode === 'day' ? handleSelectDate : openDay}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onCreateAtTime={openCreate} onCreateAtTime={openCreate}
draftResetKey={slotDraftReset}
/> />
)} )}
{viewMode === 'week' && !freeScroll && ( {viewMode === 'week' && !freeScroll && (