Block calendar swiping outside possible range

This commit is contained in:
2026-04-20 22:22:51 +02:00
parent 570e44257f
commit 63937ed7d1
2 changed files with 34 additions and 6 deletions

View File

@@ -54,7 +54,7 @@ export function createSwipePopoverCalendarHTML({
monthLabelTextClass = 'text-[10px] font-semibold leading-none tabular-nums whitespace-nowrap', monthLabelTextClass = 'text-[10px] font-semibold leading-none tabular-nums whitespace-nowrap',
}) { }) {
const weekdayHeader = ` const weekdayHeader = `
<div class="grid grid-cols-7 gap-1.5 text-center text-[8px] font-medium uppercase tracking-wide mb-1 leading-none" style="color:rgb(var(--text-dim-rgb));"> <div class="grid grid-cols-7 gap-1.5 text-center text-[8px] font-medium uppercase tracking-wide mb-1.5 leading-none" style="color:rgb(var(--text-dim-rgb));">
${weekdays.map((d) => `<div>${d}</div>`).join('')} ${weekdays.map((d) => `<div>${d}</div>`).join('')}
</div> </div>
`; `;
@@ -75,7 +75,7 @@ export function createSwipePopoverCalendarHTML({
return ` return `
<div class="pb-3 px-3 flex items-center justify-end gap-3"> <div class="pb-3 px-3 flex items-center justify-end gap-3">
<div class="flex h-[2.05rem] min-w-0 max-w-[min(100%,20rem)] items-center justify-center rounded-full border" style="background:rgb(var(--app-bg-rgb)); border-color:rgb(var(--card-raised-rgb));"> <div class="flex h-[2.05rem] min-w-0 max-w-[min(100%,20rem)] items-center justify-center rounded-full border" style="background:transparent; border-color:rgb(var(--border-input-rgb));">
<span id="${idPrefix}-month-label" class="h-full shrink-0 inline-flex min-w-[5.75rem] max-w-[9rem] items-center justify-center px-3 ${monthLabelTextClass}" style="color:rgb(var(--text-body-soft-rgb));"></span> <span id="${idPrefix}-month-label" class="h-full shrink-0 inline-flex min-w-[5.75rem] max-w-[9rem] items-center justify-center px-3 ${monthLabelTextClass}" style="color:rgb(var(--text-body-soft-rgb));"></span>
</div> </div>
</div> </div>
@@ -111,6 +111,7 @@ export function initSwipePopoverCalendar({
theme = DEFAULT_THEME, theme = DEFAULT_THEME,
getMonthAnchor, getMonthAnchor,
setMonthAnchor, setMonthAnchor,
canNavigateToMonth,
getSelectionKeys, getSelectionKeys,
onSelectionCommit, onSelectionCommit,
resolveDayState, resolveDayState,
@@ -188,6 +189,23 @@ export function initSwipePopoverCalendar({
track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`; track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`;
}; };
const getNavigationTarget = (monthDelta) => {
const anchor = normalizeMonth(getMonthAnchor());
return startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1));
};
const canNavigate = (monthDelta) => {
if (typeof canNavigateToMonth !== 'function') return true;
const anchor = normalizeMonth(getMonthAnchor());
const target = getNavigationTarget(monthDelta);
return canNavigateToMonth(target, { currentMonth: anchor, monthDelta }) !== false;
};
const getAllowedDragDx = (dx) => {
if (dx === 0) return 0;
return canNavigate(dx > 0 ? -1 : 1) ? dx : 0;
};
const getSelectedSet = (previewSelection = null) => { const getSelectedSet = (previewSelection = null) => {
const raw = previewSelection ?? getSelectionKeys(); const raw = previewSelection ?? getSelectionKeys();
if (Array.isArray(raw)) return new Set(raw); if (Array.isArray(raw)) return new Set(raw);
@@ -259,12 +277,16 @@ export function initSwipePopoverCalendar({
}; };
const commitNavigation = (monthDelta) => { const commitNavigation = (monthDelta) => {
if (!canNavigate(monthDelta)) {
setDragTranslate(0, ANIMATION_MS);
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);
return;
}
animatingNav = true; animatingNav = true;
const targetDx = monthDelta < 0 ? panelWidth : -panelWidth; const targetDx = monthDelta < 0 ? panelWidth : -panelWidth;
setDragTranslate(targetDx, ANIMATION_MS); setDragTranslate(targetDx, ANIMATION_MS);
setTimeout(() => { setTimeout(() => {
const anchor = normalizeMonth(getMonthAnchor()); setMonthAnchor(getNavigationTarget(monthDelta));
setMonthAnchor(startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1)));
render(); render();
resetTrackPosition(); resetTrackPosition();
animatingNav = false; animatingNav = false;
@@ -352,7 +374,7 @@ export function initSwipePopoverCalendar({
axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y'; axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y';
if (axis === 'x') syncGripVisibility(true); if (axis === 'x') syncGripVisibility(true);
} }
if (axis === 'x') setDragTranslate(dx, 0); if (axis === 'x') setDragTranslate(getAllowedDragDx(dx), 0);
}); });
const endGesture = (e) => { const endGesture = (e) => {
@@ -360,7 +382,8 @@ export function initSwipePopoverCalendar({
ptrId = null; ptrId = null;
if (!moved || axis !== 'x') return; if (!moved || axis !== 'x') return;
const dx = e ? e.clientX - startX : 0; const dx = e ? e.clientX - startX : 0;
if (Math.abs(dx) >= SWIPE_THRESHOLD) commitNavigation(dx > 0 ? -1 : 1); const monthDelta = dx > 0 ? -1 : 1;
if (Math.abs(dx) >= SWIPE_THRESHOLD && canNavigate(monthDelta)) commitNavigation(monthDelta);
else { else {
setDragTranslate(0, ANIMATION_MS); setDragTranslate(0, ANIMATION_MS);
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20); setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);

View File

@@ -313,6 +313,11 @@ function bindPantryCalendarInteractions() {
if (nextAnchor.getTime() < minAnchor.getTime()) return; if (nextAnchor.getTime() < minAnchor.getTime()) return;
calendarMonthAnchor = nextAnchor; calendarMonthAnchor = nextAnchor;
}, },
canNavigateToMonth: (nextMonth) => {
const nextAnchor = startOfMonth(nextMonth);
const minAnchor = startOfMonth(getToday());
return nextAnchor.getTime() >= minAnchor.getTime();
},
getSelectionKeys: () => dateKey(horizonEndDate), getSelectionKeys: () => dateKey(horizonEndDate),
onSelectionCommit: (selectedKey) => { onSelectionCommit: (selectedKey) => {
selectHorizonDate(new Date(`${selectedKey}T00:00:00`)); selectHorizonDate(new Date(`${selectedKey}T00:00:00`));