feat: show five or seven days in the multi-day calendar view on wider screens

This commit is contained in:
Linus Rath
2026-10-15 02:05:31 +02:00
parent 295e292e47
commit 2288d28e46
6 changed files with 57 additions and 14 deletions
+1
View File
@@ -134,6 +134,7 @@
"update_error": "Aufgabe konnte nicht aktualisiert werden" "update_error": "Aufgabe konnte nicht aktualisiert werden"
}, },
"views": { "views": {
"n_days": "{count} Tage",
"schedule": "Terminübersicht", "schedule": "Terminübersicht",
"three_days": "3 Tage" "three_days": "3 Tage"
} }
+1
View File
@@ -137,6 +137,7 @@
"update_error": "Failed to update task" "update_error": "Failed to update task"
}, },
"views": { "views": {
"n_days": "{count} days",
"schedule": "Schedule", "schedule": "Schedule",
"three_days": "3 days" "three_days": "3 days"
} }
@@ -61,6 +61,8 @@ type TimeFormat = '12h' | '24h';
interface TimeGridScrollViewProps { interface TimeGridScrollViewProps {
mode: TimeGridMode; mode: TimeGridMode;
/** Days of the multi-day mode (`multiDayCount`). */
multiDays?: number;
/** The day the user navigated to; its week (week view) or itself (day view) is scrolled into view. */ /** The day the user navigated to; its week (week view) or itself (day view) is scrolled into view. */
focus: CalendarFocus; focus: CalendarFocus;
/** The loaded window: one column per day. */ /** The loaded window: one column per day. */
@@ -96,6 +98,7 @@ interface TimeGridScrollViewProps {
*/ */
function TimeGridScrollViewInner({ function TimeGridScrollViewInner({
mode, mode,
multiDays = 3,
focus, focus,
window, window,
selectedDate, selectedDate,
@@ -120,7 +123,7 @@ function TimeGridScrollViewInner({
[eventsByDay, events], [eventsByDay, events],
); );
const perScreen = columnsPerScreen(mode); const perScreen = columnsPerScreen(mode, multiDays);
const { width: screenWidth } = useWindowDimensions(); const { width: screenWidth } = useWindowDimensions();
const [rootWidth, setRootWidth] = React.useState(screenWidth); const [rootWidth, setRootWidth] = React.useState(screenWidth);
const listWidth = Math.max(perScreen, rootWidth - GUTTER_WIDTH); const listWidth = Math.max(perScreen, rootWidth - GUTTER_WIDTH);
@@ -8,8 +8,10 @@ import {
} from '../calendar-scroll-window'; } from '../calendar-scroll-window';
import { import {
buildAllDaySegments, buildAllDaySegments,
columnsPerScreen,
headerColumnRange, headerColumnRange,
hourAtOffset, hourAtOffset,
multiDayCount,
timeGridFocusColumn, timeGridFocusColumn,
windowDays, windowDays,
} from '../calendar-time-grid'; } from '../calendar-time-grid';
@@ -118,3 +120,18 @@ describe('hourAtOffset', () => {
expect(hourAtOffset(-4, 48)).toBe(0); expect(hourAtOffset(-4, 48)).toBe(0);
}); });
}); });
describe('multiDayCount', () => {
it('shows three days on a phone, five on a medium window and seven on a wide one', () => {
expect(multiDayCount(411)).toBe(3);
expect(multiDayCount(800)).toBe(5);
expect(multiDayCount(1280)).toBe(7);
});
it('only changes the multi-day view', () => {
expect(columnsPerScreen('3day', 7)).toBe(7);
expect(columnsPerScreen('week', 5)).toBe(7);
expect(columnsPerScreen('day', 7)).toBe(1);
expect(columnsPerScreen('3day')).toBe(3);
});
});
+11 -2
View File
@@ -18,9 +18,18 @@ import type { DayRange, WeekStartsOn } from './calendar-scroll-window';
export type TimeGridMode = 'week' | '3day' | 'day'; export type TimeGridMode = 'week' | '3day' | 'day';
/**
* Days the multi-day view ("3 days" on a phone) shows for a window `width`
* wide: three on a phone, five on a medium window, a full seven-day strip
* on a wide one, so the extra width buys days rather than wider columns.
*/
export function multiDayCount(width: number): number {
return width >= 840 ? 7 : width >= 600 ? 5 : 3;
}
/** Day columns on one screen. */ /** Day columns on one screen. */
export function columnsPerScreen(mode: TimeGridMode): number { export function columnsPerScreen(mode: TimeGridMode, multiDays = 3): number {
return mode === 'week' ? 7 : mode === '3day' ? 3 : 1; return mode === 'week' ? 7 : mode === '3day' ? multiDays : 1;
} }
/** Every day of the window, in order. */ /** Every day of the window, in order. */
+23 -11
View File
@@ -8,8 +8,10 @@ import {
StyleSheet, StyleSheet,
Pressable, Pressable,
ActivityIndicator, ActivityIndicator,
useWindowDimensions,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { multiDayCount } from '../lib/calendar-time-grid';
import { import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
ChevronDown, ChevronDown,
@@ -110,10 +112,10 @@ function windowMode(mode: ViewMode): ScrollViewMode {
return mode === '3day' ? 'day' : mode; return mode === '3day' ? 'day' : mode;
} }
// A window without free scrolling: one period, three days for the three-day grid. // A window without free scrolling: one period, the multi-day grid's days.
function fixedWindowFor(mode: ViewMode, anchor: Date): ScrollWindowState { function fixedWindowFor(mode: ViewMode, anchor: Date, multiDays: number): ScrollWindowState {
const state = fixedScrollWindowState(windowMode(mode), anchor); const state = fixedScrollWindowState(windowMode(mode), anchor);
return mode === '3day' ? { ...state, after: 2 } : state; return mode === '3day' ? { ...state, after: multiDays - 1 } : state;
} }
type PendingAction = type PendingAction =
| { | {
@@ -189,6 +191,9 @@ export default function CalendarScreen() {
); );
// The sideways-scrolling week and day grids assume a left-to-right strip; // The sideways-scrolling week and day grids assume a left-to-right strip;
// right-to-left layouts keep them paged. // right-to-left layouts keep them paged.
// Wider windows show more days in the multi-day view.
const { width: windowWidth } = useWindowDimensions();
const multiDays = multiDayCount(windowWidth);
const freeScroll = const freeScroll =
calendarFreeScroll calendarFreeScroll
&& ((viewMode !== 'week' && viewMode !== '3day' && viewMode !== 'day') || !I18nManager.isRTL); && ((viewMode !== 'week' && viewMode !== '3day' && viewMode !== 'day') || !I18nManager.isRTL);
@@ -197,8 +202,8 @@ export default function CalendarScreen() {
() => () =>
freeScroll freeScroll
? normalizeScrollWindowState(windowState, windowMode(viewMode), parseDayKey(focusKey)) ? normalizeScrollWindowState(windowState, windowMode(viewMode), parseDayKey(focusKey))
: fixedWindowFor(viewMode, parseDayKey(focusKey)), : fixedWindowFor(viewMode, parseDayKey(focusKey), multiDays),
[freeScroll, windowState, viewMode, focusKey], [freeScroll, windowState, viewMode, focusKey, multiDays],
); );
React.useEffect(() => { React.useEffect(() => {
if (freeScroll && activeWindowState !== windowState) setWindowState(activeWindowState); if (freeScroll && activeWindowState !== windowState) setWindowState(activeWindowState);
@@ -482,21 +487,21 @@ export default function CalendarScreen() {
const base = visibleDate ?? focus.date; const base = visibleDate ?? focus.date;
jumpTo( jumpTo(
viewMode === 'week' ? subWeeks(base, 1) viewMode === 'week' ? subWeeks(base, 1)
: viewMode === '3day' ? subDays(base, 3) : viewMode === '3day' ? subDays(base, multiDays)
: viewMode === 'day' ? subDays(base, 1) : viewMode === 'day' ? subDays(base, 1)
: subMonths(base, 1), : subMonths(base, 1),
); );
}, [viewMode, visibleDate, focus.date, jumpTo]); }, [viewMode, visibleDate, focus.date, jumpTo, multiDays]);
const goNext = React.useCallback(() => { const goNext = React.useCallback(() => {
const base = visibleDate ?? focus.date; const base = visibleDate ?? focus.date;
jumpTo( jumpTo(
viewMode === 'week' ? addWeeks(base, 1) viewMode === 'week' ? addWeeks(base, 1)
: viewMode === '3day' ? addDays(base, 3) : viewMode === '3day' ? addDays(base, multiDays)
: viewMode === 'day' ? addDays(base, 1) : viewMode === 'day' ? addDays(base, 1)
: addMonths(base, 1), : addMonths(base, 1),
); );
}, [viewMode, visibleDate, focus.date, jumpTo]); }, [viewMode, visibleDate, focus.date, jumpTo, multiDays]);
const goToday = React.useCallback(() => { const goToday = React.useCallback(() => {
jumpTo(displayNow()); jumpTo(displayNow());
@@ -520,11 +525,17 @@ export default function CalendarScreen() {
() => [ () => [
{ key: 'agenda', label: t('calendar.views.schedule', 'Schedule'), Icon: Rows2 }, { key: 'agenda', label: t('calendar.views.schedule', 'Schedule'), Icon: Rows2 },
{ key: 'day', label: t('calendar.views.day', 'Day'), Icon: Square }, { key: 'day', label: t('calendar.views.day', 'Day'), Icon: Square },
{ key: '3day', label: t('calendar.views.three_days', '3 days'), Icon: Columns3 }, {
key: '3day',
label: multiDays === 3
? t('calendar.views.three_days', '3 days')
: t('calendar.views.n_days', '{count} days', { count: multiDays }),
Icon: Columns3,
},
{ key: 'week', label: t('calendar.views.week', 'Week'), Icon: Columns4 }, { key: 'week', label: t('calendar.views.week', 'Week'), Icon: Columns4 },
{ key: 'month', label: t('calendar.views.month', 'Month'), Icon: Grid3x3 }, { key: 'month', label: t('calendar.views.month', 'Month'), Icon: Grid3x3 },
], ],
[t], [t, multiDays],
); );
// The month that drops down under the title; the title follows the month // The month that drops down under the title; the title follows the month
@@ -1094,6 +1105,7 @@ export default function CalendarScreen() {
<TimeGridScrollView <TimeGridScrollView
key={windowKey} key={windowKey}
mode={viewMode} mode={viewMode}
multiDays={multiDays}
focus={focus} focus={focus}
window={scrollWindow} window={scrollWindow}
onExtendStart={freeScroll && scrollWindow.canExtendStart ? extendWindowStart : undefined} onExtendStart={freeScroll && scrollWindow.canExtendStart ? extendWindowStart : undefined}