+ // The desktop side panel is a dialog named by its title, like the event
+ // editor; on a phone the full-screen wrapper around it is.
+
{/* Header */}
-
+
{isEdit ? t("tasks.edit") : t("tasks.create")}
From 83886633af2c5f499675053b03c26946233dcba7 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 28 Oct 2026 10:39:28 +0100
Subject: [PATCH 11/15] fix: ask "delete recurring event?" and say which of its
events go when deleting a series
---
.../calendar/__tests__/calendar-app.test.tsx | 2 +-
.../recurrence-scope-dialog.test.tsx | 35 +++++++++++++++++++
components/calendar/calendar-app.tsx | 1 +
.../calendar/recurrence-scope-dialog.tsx | 18 ++++++++--
locales/ar/common.json | 4 +++
locales/ca/common.json | 4 +++
locales/cs/common.json | 4 +++
locales/da/common.json | 4 +++
locales/de/common.json | 4 +++
locales/en/common.json | 4 +++
locales/es/common.json | 4 +++
locales/et/common.json | 4 +++
locales/fa/common.json | 4 +++
locales/fr/common.json | 4 +++
locales/he/common.json | 4 +++
locales/hu/common.json | 4 +++
locales/it/common.json | 4 +++
locales/ja/common.json | 4 +++
locales/ko/common.json | 4 +++
locales/lv/common.json | 4 +++
locales/mn/common.json | 4 +++
locales/nb/common.json | 4 +++
locales/nl/common.json | 4 +++
locales/pl/common.json | 4 +++
locales/pt/common.json | 4 +++
locales/ro/common.json | 4 +++
locales/ru/common.json | 4 +++
locales/sk/common.json | 4 +++
locales/th/common.json | 4 +++
locales/tr/common.json | 4 +++
locales/uk/common.json | 4 +++
locales/zh-TW/common.json | 4 +++
locales/zh/common.json | 4 +++
33 files changed, 168 insertions(+), 4 deletions(-)
create mode 100644 components/calendar/__tests__/recurrence-scope-dialog.test.tsx
diff --git a/components/calendar/__tests__/calendar-app.test.tsx b/components/calendar/__tests__/calendar-app.test.tsx
index 24ce2f577..4b36fb2f2 100644
--- a/components/calendar/__tests__/calendar-app.test.tsx
+++ b/components/calendar/__tests__/calendar-app.test.tsx
@@ -402,7 +402,7 @@ describe('CalendarApp deleting', () => {
renderApp();
fireEvent.click(screen.getAllByRole('button', { name: /Standup/ })[0]);
fireEvent.click(within(screen.getByRole('dialog', { name: 'Standup' })).getByRole('button', { name: 'events.delete' }));
- expect(await screen.findByRole('dialog', { name: 'delete_title' })).toBeInTheDocument();
+ expect(await screen.findByRole('dialog', { name: 'delete_question' })).toBeInTheDocument();
expect(screen.queryByRole('alertdialog')).toBeNull();
});
diff --git a/components/calendar/__tests__/recurrence-scope-dialog.test.tsx b/components/calendar/__tests__/recurrence-scope-dialog.test.tsx
new file mode 100644
index 000000000..7f93c7c81
--- /dev/null
+++ b/components/calendar/__tests__/recurrence-scope-dialog.test.tsx
@@ -0,0 +1,35 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import { RecurrenceScopeDialog } from '../recurrence-scope-dialog';
+
+// The delete scope dialog asked "Which events would you like to modify?":
+// no word of what is lost (R3), and a title without a question mark.
+
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string, values?: Record) =>
+ values ? `${key}(${Object.values(values).join(',')})` : key,
+}));
+
+describe('RecurrenceScopeDialog', () => {
+ it('asks to delete with a question and says what goes for the chosen scope', () => {
+ render(
+ ,
+ );
+ const dialog = screen.getByRole('dialog', { name: 'delete_question' });
+ expect(dialog).toHaveAccessibleDescription('delete_this_message(Yoga)');
+ fireEvent.click(screen.getByLabelText('all_events'));
+ expect(dialog).toHaveAccessibleDescription('delete_all_message(Yoga)');
+ fireEvent.click(screen.getByLabelText('this_and_future'));
+ expect(dialog).toHaveAccessibleDescription('delete_following_message(Yoga)');
+ // The cancellation note for an organizer's event with participants
+ expect(screen.getByText('cancel_notification')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'delete' }).className).toMatch(/destructive/);
+ });
+
+ it('keeps the edit wording for an edit', () => {
+ render();
+ const dialog = screen.getByRole('dialog', { name: 'edit_title' });
+ expect(dialog).toHaveAccessibleDescription('description');
+ expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument();
+ });
+});
diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx
index b56e29ceb..c5bfdb825 100644
--- a/components/calendar/calendar-app.tsx
+++ b/components/calendar/calendar-app.tsx
@@ -2437,6 +2437,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
&& userOrganizes(pendingScopeAction.event, currentUserEmails)
? t("participants.cancel_notification")
: undefined}
+ eventTitle={pendingScopeAction?.event.title || t("events.no_title")}
onSelect={handleScopeSelect}
onClose={() => setPendingScopeAction(null)}
/>
diff --git a/components/calendar/recurrence-scope-dialog.tsx b/components/calendar/recurrence-scope-dialog.tsx
index 8f79bf84b..dc7e4422f 100644
--- a/components/calendar/recurrence-scope-dialog.tsx
+++ b/components/calendar/recurrence-scope-dialog.tsx
@@ -15,14 +15,23 @@ interface RecurrenceScopeDialogProps {
onClose: () => void;
/** A line under the question, e.g. that the participants get a cancellation. */
note?: string;
+ /** The series' title: a delete says which of its events go (R3). */
+ eventTitle?: string;
}
+const DELETE_MESSAGES: Record = {
+ this: "delete_this_message",
+ this_and_future: "delete_following_message",
+ all: "delete_all_message",
+};
+
export function RecurrenceScopeDialog({
isOpen,
actionType,
onSelect,
onClose,
note,
+ eventTitle = "",
}: RecurrenceScopeDialogProps) {
const t = useTranslations("calendar.recurrence_scope");
const id = useId();
@@ -72,10 +81,13 @@ export function RecurrenceScopeDialog({
- {isDelete ? t("delete_title") : isRsvp ? t("rsvp_title") : t("edit_title")}
+ {isDelete ? t("delete_question") : isRsvp ? t("rsvp_title") : t("edit_title")}
-
- {isRsvp ? t("rsvp_description") : t("description")}
+ {/* A delete says what goes with the chosen scope, and that it is for good. */}
+
+ {isDelete
+ ? t(DELETE_MESSAGES[choice], { title: eventTitle })
+ : isRsvp ? t("rsvp_description") : t("description")}
{note &&
{note}
}
diff --git a/locales/ar/common.json b/locales/ar/common.json
index 8362d7178..bd7610dae 100644
--- a/locales/ar/common.json
+++ b/locales/ar/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "تعديل حدث متكرر",
"delete_title": "حذف حدث متكرر",
+ "delete_question": "حذف الحدث المتكرر؟",
+ "delete_this_message": "سيتم حذف هذا التكرار فقط من \"{title}\". لا يمكن التراجع عن ذلك.",
+ "delete_following_message": "سيتم حذف هذا التكرار وكل التكرارات التالية من \"{title}\". لا يمكن التراجع عن ذلك.",
+ "delete_all_message": "سيتم حذف كل تكرارات \"{title}\". لا يمكن التراجع عن ذلك.",
"description": "هذا حدث متكرر. ما الأحداث التي تريد تعديلها؟",
"this_event": "هذا الحدث فقط",
"this_and_future": "هذا الحدث والأحداث التالية",
diff --git a/locales/ca/common.json b/locales/ca/common.json
index 93443e066..37275c320 100644
--- a/locales/ca/common.json
+++ b/locales/ca/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Edita l'esdeveniment periòdic",
"delete_title": "Suprimeix l'esdeveniment periòdic",
+ "delete_question": "Voleu suprimir l'esdeveniment periòdic?",
+ "delete_this_message": "Només se suprimirà aquesta repetició de «{title}». No es pot desfer.",
+ "delete_following_message": "Se suprimiran aquesta repetició i totes les següents de «{title}». No es pot desfer.",
+ "delete_all_message": "Se suprimiran totes les repeticions de «{title}». No es pot desfer.",
"description": "Aquest és un esdeveniment periòdic. Quins esdeveniments voleu modificar?",
"this_event": "Només aquest esdeveniment",
"this_and_future": "Aquest i els següents esdeveniments",
diff --git a/locales/cs/common.json b/locales/cs/common.json
index 9a36130c2..0c6ff7da4 100644
--- a/locales/cs/common.json
+++ b/locales/cs/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Upravit opakující se událost",
"delete_title": "Odstranit opakující se událost",
+ "delete_question": "Odstranit opakující se událost?",
+ "delete_this_message": "Odstraní se pouze tento výskyt události \"{title}\". Tuto akci nelze vrátit.",
+ "delete_following_message": "Odstraní se tento a všechny další výskyty události \"{title}\". Tuto akci nelze vrátit.",
+ "delete_all_message": "Odstraní se všechny výskyty události \"{title}\". Tuto akci nelze vrátit.",
"description": "Toto je opakující se událost. Které události chcete upravit?",
"this_event": "Pouze tuto událost",
"this_and_future": "Tuto a všechny následující události",
diff --git a/locales/da/common.json b/locales/da/common.json
index 3a02cdc78..06a3cbef9 100644
--- a/locales/da/common.json
+++ b/locales/da/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Redigér tilbagevendende begivenhed",
"delete_title": "Slet tilbagevendende begivenhed",
+ "delete_question": "Slet tilbagevendende begivenhed?",
+ "delete_this_message": "Kun denne forekomst af \"{title}\" bliver slettet. Det kan ikke fortrydes.",
+ "delete_following_message": "Denne og alle senere forekomster af \"{title}\" bliver slettet. Det kan ikke fortrydes.",
+ "delete_all_message": "Alle forekomster af \"{title}\" bliver slettet. Det kan ikke fortrydes.",
"description": "Dette er en tilbagevendende begivenhed. Hvilke begivenheder vil du ændre?",
"this_event": "Kun denne begivenhed",
"this_and_future": "Denne og fremtidige begivenheder",
diff --git a/locales/de/common.json b/locales/de/common.json
index c48ca51e2..5d3fa2185 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Wiederkehrendes Ereignis bearbeiten",
"delete_title": "Wiederkehrendes Ereignis löschen",
+ "delete_question": "Wiederkehrenden Termin löschen?",
+ "delete_this_message": "Nur dieser Termin von „{title}“ wird gelöscht. Das kann nicht rückgängig gemacht werden.",
+ "delete_following_message": "Dieser und alle folgenden Termine von „{title}“ werden gelöscht. Das kann nicht rückgängig gemacht werden.",
+ "delete_all_message": "Alle Termine von „{title}“ werden gelöscht. Das kann nicht rückgängig gemacht werden.",
"description": "Dies ist ein wiederkehrendes Ereignis. Welche Ereignisse möchten Sie ändern?",
"this_event": "Nur dieses Ereignis",
"this_and_future": "Dieses und folgende Ereignisse",
diff --git a/locales/en/common.json b/locales/en/common.json
index 4329039cf..973a445cf 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Edit recurring event",
"delete_title": "Delete recurring event",
+ "delete_question": "Delete recurring event?",
+ "delete_this_message": "Only this occurrence of \"{title}\" will be deleted. This can't be undone.",
+ "delete_following_message": "This and every later occurrence of \"{title}\" will be deleted. This can't be undone.",
+ "delete_all_message": "Every occurrence of \"{title}\" will be deleted. This can't be undone.",
"description": "This is a recurring event. Which events would you like to modify?",
"this_event": "This event only",
"this_and_future": "This and following events",
diff --git a/locales/es/common.json b/locales/es/common.json
index 681722895..0446248c1 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Editar evento recurrente",
"delete_title": "Eliminar evento recurrente",
+ "delete_question": "¿Eliminar evento recurrente?",
+ "delete_this_message": "Solo se eliminará esta repetición de \"{title}\". No se puede deshacer.",
+ "delete_following_message": "Se eliminarán esta repetición y todas las siguientes de \"{title}\". No se puede deshacer.",
+ "delete_all_message": "Se eliminarán todas las repeticiones de \"{title}\". No se puede deshacer.",
"description": "Este es un evento recurrente. ¿Qué eventos desea modificar?",
"this_event": "Solo este evento",
"this_and_future": "Este evento y los siguientes",
diff --git a/locales/et/common.json b/locales/et/common.json
index 6677299c0..ae3379813 100644
--- a/locales/et/common.json
+++ b/locales/et/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Korduva sündmuse muutmine",
"delete_title": "Korduva sündmuse kustutamine",
+ "delete_question": "Kas kustutada korduv sündmus?",
+ "delete_this_message": "Kustutatakse ainult see \"{title}\" kordus. Seda ei saa tagasi võtta.",
+ "delete_following_message": "Kustutatakse see ja kõik järgmised \"{title}\" kordused. Seda ei saa tagasi võtta.",
+ "delete_all_message": "Kustutatakse kõik \"{title}\" kordused. Seda ei saa tagasi võtta.",
"description": "See on korduv sündmus. Milliseid sündmusi soovid muuta?",
"this_event": "Ainult see sündmus",
"this_and_future": "See ja järgnevad sündmused",
diff --git a/locales/fa/common.json b/locales/fa/common.json
index 32557ba10..c43917681 100644
--- a/locales/fa/common.json
+++ b/locales/fa/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "ویرایش رویداد تکراری",
"delete_title": "حذف رویداد تکراری",
+ "delete_question": "رویداد تکراری حذف شود؟",
+ "delete_this_message": "فقط همین تکرارِ \"{title}\" حذف خواهد شد. این کار قابل بازگشت نیست.",
+ "delete_following_message": "این تکرار و همه تکرارهای بعدیِ \"{title}\" حذف خواهند شد. این کار قابل بازگشت نیست.",
+ "delete_all_message": "همه تکرارهای \"{title}\" حذف خواهند شد. این کار قابل بازگشت نیست.",
"description": "این یک رویداد تکراری است. کدام رویدادها را میخواهید تغییر دهید؟",
"this_event": "فقط این رویداد",
"this_and_future": "این رویداد و رویدادهای بعدی",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 45c46b169..2384f1255 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Modifier l'événement récurrent",
"delete_title": "Supprimer l'événement récurrent",
+ "delete_question": "Supprimer l'événement récurrent ?",
+ "delete_this_message": "Seule cette occurrence de « {title} » sera supprimée. Cette action est irréversible.",
+ "delete_following_message": "Cette occurrence et toutes les suivantes de « {title} » seront supprimées. Cette action est irréversible.",
+ "delete_all_message": "Toutes les occurrences de « {title} » seront supprimées. Cette action est irréversible.",
"description": "Ceci est un événement récurrent. Quels événements souhaitez-vous modifier ?",
"this_event": "Cet événement uniquement",
"this_and_future": "Cet événement et les suivants",
diff --git a/locales/he/common.json b/locales/he/common.json
index af15c6fe9..8b626f13f 100644
--- a/locales/he/common.json
+++ b/locales/he/common.json
@@ -3692,6 +3692,10 @@
"recurrence_scope": {
"edit_title": "ערוך אירוע חוזר",
"delete_title": "מחק אירוע חוזר",
+ "delete_question": "למחוק את האירוע החוזר?",
+ "delete_this_message": "רק המופע הזה של \"{title}\" יימחק. לא ניתן לבטל פעולה זו.",
+ "delete_following_message": "המופע הזה וכל המופעים הבאים של \"{title}\" יימחקו. לא ניתן לבטל פעולה זו.",
+ "delete_all_message": "כל המופעים של \"{title}\" יימחקו. לא ניתן לבטל פעולה זו.",
"description": "זהו אירוע שחוזר על עצמו. אילו אירועים תרצה לשנות?",
"this_event": "אירוע זה בלבד",
"this_and_future": "זה והאירועים הבאים",
diff --git a/locales/hu/common.json b/locales/hu/common.json
index 4aa36c35c..49d858db6 100644
--- a/locales/hu/common.json
+++ b/locales/hu/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Ismétlődő esemény szerkesztése",
"delete_title": "Ismétlődő esemény törlése",
+ "delete_question": "Törli az ismétlődő eseményt?",
+ "delete_this_message": "Csak a(z) \"{title}\" esemény ezen alkalma törlődik. Ez nem vonható vissza.",
+ "delete_following_message": "A(z) \"{title}\" esemény ezen és minden későbbi alkalma törlődik. Ez nem vonható vissza.",
+ "delete_all_message": "A(z) \"{title}\" esemény minden alkalma törlődik. Ez nem vonható vissza.",
"description": "Ez egy ismétlődő esemény. Mely eseményeket szeretnéd módosítani?",
"this_event": "Csak ez az esemény",
"this_and_future": "Ez és a következő események",
diff --git a/locales/it/common.json b/locales/it/common.json
index 4d5ec0030..118458e91 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Modifica evento ricorrente",
"delete_title": "Elimina evento ricorrente",
+ "delete_question": "Eliminare l'evento ricorrente?",
+ "delete_this_message": "Verrà eliminata solo questa occorrenza di \"{title}\". L'operazione non può essere annullata.",
+ "delete_following_message": "Verranno eliminate questa occorrenza e tutte le successive di \"{title}\". L'operazione non può essere annullata.",
+ "delete_all_message": "Verranno eliminate tutte le occorrenze di \"{title}\". L'operazione non può essere annullata.",
"description": "Questo è un evento ricorrente. Quali eventi desideri modificare?",
"this_event": "Solo questo evento",
"this_and_future": "Questo evento e i seguenti",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 5d6d4771c..4624ab95a 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "繰り返しイベントを編集",
"delete_title": "繰り返しイベントを削除",
+ "delete_question": "繰り返しの予定を削除しますか?",
+ "delete_this_message": "「{title}」のこの回だけを削除します。元に戻すことはできません。",
+ "delete_following_message": "「{title}」のこの回以降をすべて削除します。元に戻すことはできません。",
+ "delete_all_message": "「{title}」のすべての回を削除します。元に戻すことはできません。",
"description": "これは繰り返しイベントです。どのイベントを変更しますか?",
"this_event": "このイベントのみ",
"this_and_future": "これ以降のすべてのイベント",
diff --git a/locales/ko/common.json b/locales/ko/common.json
index 5f5e78516..200af04b8 100644
--- a/locales/ko/common.json
+++ b/locales/ko/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "반복 일정 수정",
"delete_title": "반복 일정 삭제",
+ "delete_question": "반복 일정을 삭제할까요?",
+ "delete_this_message": "\"{title}\"의 이번 일정만 삭제됩니다. 되돌릴 수 없습니다.",
+ "delete_following_message": "\"{title}\"의 이번 일정과 이후 모든 일정이 삭제됩니다. 되돌릴 수 없습니다.",
+ "delete_all_message": "\"{title}\"의 모든 일정이 삭제됩니다. 되돌릴 수 없습니다.",
"description": "반복되는 일정이에요. 어떤 일정을 수정할까요?",
"this_event": "이 일정만",
"this_and_future": "이 일정 및 향후 일정 모두",
diff --git a/locales/lv/common.json b/locales/lv/common.json
index 4abab7cdc..11185479f 100644
--- a/locales/lv/common.json
+++ b/locales/lv/common.json
@@ -3773,6 +3773,10 @@
"recurrence_scope": {
"edit_title": "Rediģēt atkārtotu pasākumu",
"delete_title": "Dzēst atkārtotu pasākumu",
+ "delete_question": "Dzēst atkārtotu notikumu?",
+ "delete_this_message": "Tiks dzēsta tikai šī «{title}» reize. To nevar atsaukt.",
+ "delete_following_message": "Tiks dzēsta šī un visas nākamās «{title}» reizes. To nevar atsaukt.",
+ "delete_all_message": "Tiks dzēstas visas «{title}» reizes. To nevar atsaukt.",
"description": "Šis ir atkārtots pasākums. Kuras instances vēlaties mainīt?",
"this_event": "Tikai šo pasākumu",
"this_and_future": "Šo un turpmākos",
diff --git a/locales/mn/common.json b/locales/mn/common.json
index 6e29449b6..0b72f4a33 100644
--- a/locales/mn/common.json
+++ b/locales/mn/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Давтах үйл явдлыг засах",
"delete_title": "Дахин давтагдах үйл явдлыг устгах",
+ "delete_question": "Давтагдах арга хэмжээг устгах уу?",
+ "delete_this_message": "\"{title}\"-ийн зөвхөн энэ удаагийнх устгагдана. Үүнийг буцаах боломжгүй.",
+ "delete_following_message": "\"{title}\"-ийн энэ болон дараагийн бүх удаагийнх устгагдана. Үүнийг буцаах боломжгүй.",
+ "delete_all_message": "\"{title}\"-ийн бүх удаагийнх устгагдана. Үүнийг буцаах боломжгүй.",
"description": "Энэ бол давтагдах үйл явдал. Та ямар үйл явдлыг өөрчлөхийг хүсэж байна вэ?",
"this_event": "Зөвхөн энэ арга хэмжээ",
"this_and_future": "Энэ болон дараах үйл явдлууд",
diff --git a/locales/nb/common.json b/locales/nb/common.json
index 316709528..5a82da2de 100644
--- a/locales/nb/common.json
+++ b/locales/nb/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Rediger gjentakende hendelse",
"delete_title": "Slett gjentakende hendelse",
+ "delete_question": "Vil du slette den gjentakende hendelsen?",
+ "delete_this_message": "Bare denne forekomsten av «{title}» blir slettet. Dette kan ikke angres.",
+ "delete_following_message": "Denne og alle senere forekomster av «{title}» blir slettet. Dette kan ikke angres.",
+ "delete_all_message": "Alle forekomster av «{title}» blir slettet. Dette kan ikke angres.",
"description": "Dette er en gjentakende hendelse. Hvilke hendelser vil du endre?",
"this_event": "Bare denne hendelsen",
"this_and_future": "Dette og følgende hendelser",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index a9d8f202d..945dbd2ef 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Terugkerend evenement bewerken",
"delete_title": "Terugkerend evenement verwijderen",
+ "delete_question": "Terugkerende afspraak verwijderen?",
+ "delete_this_message": "Alleen deze keer van \"{title}\" wordt verwijderd. Dit kan niet ongedaan worden gemaakt.",
+ "delete_following_message": "Deze en alle volgende keren van \"{title}\" worden verwijderd. Dit kan niet ongedaan worden gemaakt.",
+ "delete_all_message": "Alle keren van \"{title}\" worden verwijderd. Dit kan niet ongedaan worden gemaakt.",
"description": "Dit is een terugkerend evenement. Welke evenementen wilt u wijzigen?",
"this_event": "Alleen dit evenement",
"this_and_future": "Dit en volgende evenementen",
diff --git a/locales/pl/common.json b/locales/pl/common.json
index 69c043ef9..8d007bb53 100644
--- a/locales/pl/common.json
+++ b/locales/pl/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Edytuj wydarzenie cykliczne",
"delete_title": "Usuń wydarzenie cykliczne",
+ "delete_question": "Usunąć wydarzenie cykliczne?",
+ "delete_this_message": "Zostanie usunięte tylko to wystąpienie \"{title}\". Tej operacji nie można cofnąć.",
+ "delete_following_message": "Zostaną usunięte to i wszystkie następne wystąpienia \"{title}\". Tej operacji nie można cofnąć.",
+ "delete_all_message": "Zostaną usunięte wszystkie wystąpienia \"{title}\". Tej operacji nie można cofnąć.",
"description": "To jest wydarzenie cykliczne. Które wydarzenia chcesz zmodyfikować?",
"this_event": "Tylko to wydarzenie",
"this_and_future": "To i następne wydarzenia",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 34febc6cd..92259d40f 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Editar evento recorrente",
"delete_title": "Excluir evento recorrente",
+ "delete_question": "Excluir evento recorrente?",
+ "delete_this_message": "Somente esta ocorrência de \"{title}\" será excluída. Isso não pode ser desfeito.",
+ "delete_following_message": "Esta e todas as ocorrências seguintes de \"{title}\" serão excluídas. Isso não pode ser desfeito.",
+ "delete_all_message": "Todas as ocorrências de \"{title}\" serão excluídas. Isso não pode ser desfeito.",
"description": "Este é um evento recorrente. Quais eventos você deseja modificar?",
"this_event": "Apenas este evento",
"this_and_future": "Este evento e os seguintes",
diff --git a/locales/ro/common.json b/locales/ro/common.json
index c888e1c34..638904294 100644
--- a/locales/ro/common.json
+++ b/locales/ro/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Editați evenimentul recurent",
"delete_title": "Șterge evenimentul recurent",
+ "delete_question": "Ștergeți evenimentul recurent?",
+ "delete_this_message": "Va fi ștearsă doar această apariție a „{title}”. Acțiunea nu poate fi anulată.",
+ "delete_following_message": "Vor fi șterse această apariție și toate cele următoare ale „{title}”. Acțiunea nu poate fi anulată.",
+ "delete_all_message": "Vor fi șterse toate aparițiile „{title}”. Acțiunea nu poate fi anulată.",
"description": "Acesta este un eveniment recurent. Ce evenimente doriți să modificați?",
"this_event": "Numai acest eveniment",
"this_and_future": "Acest eveniment și evenimentele următoare",
diff --git a/locales/ru/common.json b/locales/ru/common.json
index 26248fc12..1b348bd51 100644
--- a/locales/ru/common.json
+++ b/locales/ru/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Редактировать повторяющееся событие",
"delete_title": "Удалить повторяющееся событие",
+ "delete_question": "Удалить повторяющееся событие?",
+ "delete_this_message": "Будет удалено только это повторение «{title}». Это действие нельзя отменить.",
+ "delete_following_message": "Будут удалены это и все следующие повторения «{title}». Это действие нельзя отменить.",
+ "delete_all_message": "Будут удалены все повторения «{title}». Это действие нельзя отменить.",
"description": "Это повторяющееся событие. Какие события вы хотите изменить?",
"this_event": "Только это событие",
"this_and_future": "Это и последующие события",
diff --git a/locales/sk/common.json b/locales/sk/common.json
index 5f78df7b6..cafdc77ad 100644
--- a/locales/sk/common.json
+++ b/locales/sk/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Upraviť opakujúcu sa udalosť",
"delete_title": "Odstrániť opakujúcu sa udalosť",
+ "delete_question": "Odstrániť opakujúcu sa udalosť?",
+ "delete_this_message": "Odstráni sa iba tento výskyt udalosti \"{title}\". Túto akciu nemožno vrátiť.",
+ "delete_following_message": "Odstráni sa tento a všetky ďalšie výskyty udalosti \"{title}\". Túto akciu nemožno vrátiť.",
+ "delete_all_message": "Odstránia sa všetky výskyty udalosti \"{title}\". Túto akciu nemožno vrátiť.",
"description": "Toto je opakujúca sa udalosť. Ktoré udalosti chcete upraviť?",
"this_event": "Iba túto udalosť",
"this_and_future": "Túto a všetky nasledujúce udalosti",
diff --git a/locales/th/common.json b/locales/th/common.json
index 191606baa..62defd1b8 100644
--- a/locales/th/common.json
+++ b/locales/th/common.json
@@ -2898,6 +2898,10 @@
"recurrence_scope": {
"edit_title": "แก้ไขกิจกรรมที่เกิดซ้ำ",
"delete_title": "ลบกิจกรรมที่เกิดซ้ำ",
+ "delete_question": "ลบกิจกรรมที่เกิดซ้ำหรือไม่?",
+ "delete_this_message": "จะลบเฉพาะครั้งนี้ของ “{title}” และไม่สามารถเรียกคืนได้",
+ "delete_following_message": "จะลบครั้งนี้และทุกครั้งถัดไปของ “{title}” และไม่สามารถเรียกคืนได้",
+ "delete_all_message": "จะลบทุกครั้งของ “{title}” และไม่สามารถเรียกคืนได้",
"description": "นี่คือกิจกรรมที่เกิดซ้ำ คุณต้องการปรับเปลี่ยนกิจกรรมใดบ้าง?",
"this_event": "เฉพาะกิจกรรมนี้",
"this_and_future": "กิจกรรมนี้และกิจกรรมที่ตามมา",
diff --git a/locales/tr/common.json b/locales/tr/common.json
index ccb1b0544..71edc44bc 100644
--- a/locales/tr/common.json
+++ b/locales/tr/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Yinelenen etkinliği düzenle",
"delete_title": "Yinelenen etkinliği sil",
+ "delete_question": "Yinelenen etkinlik silinsin mi?",
+ "delete_this_message": "\"{title}\" etkinliğinin yalnızca bu tekrarı silinecek. Bu işlem geri alınamaz.",
+ "delete_following_message": "\"{title}\" etkinliğinin bu ve sonraki tüm tekrarları silinecek. Bu işlem geri alınamaz.",
+ "delete_all_message": "\"{title}\" etkinliğinin tüm tekrarları silinecek. Bu işlem geri alınamaz.",
"description": "Bu yinelenen bir etkinlik. Hangi etkinlikleri değiştirmek istersiniz?",
"this_event": "Yalnızca bu etkinlik",
"this_and_future": "Bu ve sonraki etkinlikler",
diff --git a/locales/uk/common.json b/locales/uk/common.json
index 735d06df1..7398275dd 100644
--- a/locales/uk/common.json
+++ b/locales/uk/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "Редагувати повторювану подію",
"delete_title": "Видалити повторювану подію",
+ "delete_question": "Видалити повторювану подію?",
+ "delete_this_message": "Буде видалено лише це повторення «{title}». Цю дію не можна скасувати.",
+ "delete_following_message": "Буде видалено це й усі наступні повторення «{title}». Цю дію не можна скасувати.",
+ "delete_all_message": "Буде видалено всі повторення «{title}». Цю дію не можна скасувати.",
"description": "Це повторювана подія. Які події ви б хотіли змінити?",
"this_event": "Тільки ця подія",
"this_and_future": "Ця та наступні події",
diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json
index 904bc88de..3bea7b652 100644
--- a/locales/zh-TW/common.json
+++ b/locales/zh-TW/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "編輯重複活動",
"delete_title": "刪除重複活動",
+ "delete_question": "要刪除重複活動嗎?",
+ "delete_this_message": "僅刪除「{title}」的這一次,且無法復原。",
+ "delete_following_message": "將刪除「{title}」的這一次及之後的所有重複,且無法復原。",
+ "delete_all_message": "將刪除「{title}」的所有重複,且無法復原。",
"description": "這是重複活動。您要修改哪些活動?",
"this_event": "僅限本次活動",
"this_and_future": "本次及後續活動",
diff --git a/locales/zh/common.json b/locales/zh/common.json
index c0d17e304..4be939f45 100644
--- a/locales/zh/common.json
+++ b/locales/zh/common.json
@@ -3774,6 +3774,10 @@
"recurrence_scope": {
"edit_title": "编辑重复事件",
"delete_title": "删除重复事件",
+ "delete_question": "删除重复事件?",
+ "delete_this_message": "仅删除“{title}”的这一次,且无法撤销。",
+ "delete_following_message": "将删除“{title}”的这一次及之后的所有重复,且无法撤销。",
+ "delete_all_message": "将删除“{title}”的所有重复,且无法撤销。",
"description": "这是一个重复事件。您想修改哪些事件?",
"this_event": "仅限本次活动",
"this_and_future": "本次及后续活动",
From 87323d4137b79b126e7528abc3bf742c428838b1 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 28 Oct 2026 10:45:42 +0100
Subject: [PATCH 12/15] fix: keep tab in the week or day in view instead of
walking into the hidden days on either side
---
.../calendar-week-view-tab-order.test.tsx | 90 +++++++++++++++++++
components/calendar/calendar-day-view.tsx | 10 +++
components/calendar/calendar-week-view.tsx | 17 ++++
.../calendar/display-hours-controls.tsx | 5 +-
components/calendar/event-card.tsx | 7 +-
components/calendar/task-chip.tsx | 6 +-
hooks/use-time-grid-focus.ts | 26 ++++--
7 files changed, 151 insertions(+), 10 deletions(-)
create mode 100644 components/calendar/__tests__/calendar-week-view-tab-order.test.tsx
diff --git a/components/calendar/__tests__/calendar-week-view-tab-order.test.tsx b/components/calendar/__tests__/calendar-week-view-tab-order.test.tsx
new file mode 100644
index 000000000..a188afdf6
--- /dev/null
+++ b/components/calendar/__tests__/calendar-week-view-tab-order.test.tsx
@@ -0,0 +1,90 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import { CalendarWeekView } from '../calendar-week-view';
+import { CalendarDayView } from '../calendar-day-view';
+import type { Calendar, CalendarEvent } from '@/lib/jmap/types';
+
+// Tab in the week view walked into the events of the weeks loaded on
+// either side and scrolled the grid off its week.
+
+const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[];
+
+function makeEvent(id: string, title: string, start: string, extra: Partial
= {}): CalendarEvent {
+ return {
+ id, uid: id, '@type': 'Event', title, start, duration: 'PT1H', showWithoutTime: false,
+ calendarIds: { 'cal-1': true }, ...extra,
+ } as unknown as CalendarEvent;
+}
+
+const events = [
+ makeEvent('before', 'Last week', '2026-09-02T10:00:00'),
+ makeEvent('now', 'This week', '2026-09-09T10:00:00'),
+ makeEvent('after', 'Next week', '2026-09-16T10:00:00'),
+ makeEvent('trip', 'Trip', '2026-09-03T00:00:00', { showWithoutTime: true, duration: 'P1D' }),
+ makeEvent('fair', 'Fair', '2026-09-08T00:00:00', { showWithoutTime: true, duration: 'P1D' }),
+];
+
+const tabbable = (name: RegExp) => screen.getByRole('button', { name }).tabIndex >= 0;
+
+describe('tab order of the week view', () => {
+ it('leaves the events of the weeks on either side out of the tab order', () => {
+ render(
+ ,
+ );
+ expect(tabbable(/^This week/)).toBe(true);
+ expect(tabbable(/^Fair/)).toBe(true);
+ expect(tabbable(/^Last week/)).toBe(false);
+ expect(tabbable(/^Next week/)).toBe(false);
+ expect(tabbable(/^Trip/)).toBe(false);
+ });
+
+ it('keeps the grid tab stop on a day in view when the selected day is scrolled away', () => {
+ render(
+ ,
+ );
+ const stops = screen.getAllByRole('gridcell').filter((cell) => cell.tabIndex === 0);
+ expect(stops).toHaveLength(1);
+ expect(stops[0].getAttribute('data-slot')).toMatch(/^2026-09-07T/);
+ });
+
+ it('does the same in the day view', () => {
+ render(
+ ,
+ );
+ expect(tabbable(/^Tuesday/)).toBe(true);
+ expect(tabbable(/^Monday/)).toBe(false);
+ });
+});
diff --git a/components/calendar/calendar-day-view.tsx b/components/calendar/calendar-day-view.tsx
index b2518068b..0d532d52c 100644
--- a/components/calendar/calendar-day-view.tsx
+++ b/components/calendar/calendar-day-view.tsx
@@ -295,6 +295,10 @@ export function CalendarDayView({
onMoveEvent,
});
+ // Tab stays in the day in view: the loaded days on either side are out of
+ // the tab order, or Tab walked into them and scrolled the strip.
+ const isInView = useCallback((key: string) => key === visibleKey, [visibleKey]);
+
const { slotProps } = useTimeGridFocus({
rootRef,
headerRef,
@@ -303,6 +307,7 @@ export function CalendarDayView({
hours,
selectedDate,
onCreate: onCreateAtTime,
+ isInView,
});
const formatHour = (h: number): string => {
@@ -408,6 +413,7 @@ export function CalendarDayView({
onMouseLeave={onHoverLeave}
onContextMenu={onContextMenuEvent}
currentUserEmails={currentUserEmails}
+ tabbable={key === visibleKey}
/>
);
})}
@@ -427,6 +433,7 @@ export function CalendarDayView({
onToggleComplete={onToggleTaskComplete}
onSelect={onSelectTask}
touch={isMobile}
+ tabbable={key === visibleKey}
/>
))}
@@ -504,6 +511,7 @@ export function CalendarDayView({
count={dayLayout.before}
direction="before"
onReveal={() => revealMinutes(dayLayout.firstBeforeMinutes ?? 0)}
+ tabbable={key === visibleKey}
/>
)}
{dayLayout && dayLayout.after > 0 && (
@@ -511,6 +519,7 @@ export function CalendarDayView({
count={dayLayout.after}
direction="after"
onReveal={() => revealMinutes(dayLayout.firstAfterMinutes ?? hours.endMinutes)}
+ tabbable={key === visibleKey}
/>
)}
@@ -548,6 +557,7 @@ export function CalendarDayView({
onMouseLeave={onHoverLeave}
onContextMenu={onContextMenuEvent}
currentUserEmails={currentUserEmails}
+ tabbable={key === visibleKey}
draggable={editable}
continuesBefore={clip.clippedStart}
continuesAfter={clip.clippedEnd}
diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx
index 506e394d4..892db04eb 100644
--- a/components/calendar/calendar-week-view.tsx
+++ b/components/calendar/calendar-week-view.tsx
@@ -394,6 +394,17 @@ export function CalendarWeekView({
onMoveEvent,
});
+ // The days in view. Tab stays in them: events of the weeks loaded on
+ // either side are out of the tab order, or Tab walked into them and
+ // scrolled the grid off its week.
+ const inViewKeys = useMemo(
+ () => new Set(days.slice(visibleStartIndex, visibleStartIndex + visibleColumns).map(dayKey)),
+ [days, visibleStartIndex, visibleColumns],
+ );
+ const isInView = useCallback((key: string) => inViewKeys.has(key), [inViewKeys]);
+ const spanInView = (startIndex: number, span: number) =>
+ startIndex < visibleStartIndex + visibleColumns && startIndex + span > visibleStartIndex;
+
const { slotProps, tabStopDayKey } = useTimeGridFocus({
rootRef,
headerRef,
@@ -402,6 +413,7 @@ export function CalendarWeekView({
hours,
selectedDate,
onCreate: onCreateAtTime,
+ isInView,
});
const formatHour = (h: number): string => {
@@ -553,6 +565,7 @@ export function CalendarWeekView({
onMouseLeave={onHoverLeave}
onContextMenu={onContextMenuEvent}
currentUserEmails={currentUserEmails}
+ tabbable={spanInView(segment.startIndex, segment.span)}
/>