From a899c713df513c680976773f9a4214ec9a3568f5 Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 12:20:37 +0100 Subject: [PATCH 01/13] add weekly days input --- .../schedulerRecurringEventSelectors.ts | 16 ++++++ .../components/event-popover/EventPopover.css | 2 +- .../components/event-popover/FormContent.tsx | 28 +++++++++-- .../event-popover/RecurrenceTab.tsx | 49 ++++++++++++++++++- .../x-scheduler/src/models/translations.ts | 1 + .../src/styles/components/menus.css | 27 ++++++++++ packages/x-scheduler/src/translations/enUS.ts | 1 + 7 files changed, 118 insertions(+), 6 deletions(-) diff --git a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts index 8ce190534acea..b236ce814f876 100644 --- a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts +++ b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts @@ -2,7 +2,9 @@ import { createSelector, createSelectorMemoized } from '@base-ui-components/util import { RecurringEventPresetKey, RecurringEventRecurrenceRule, + RecurringEventWeekDayCode, SchedulerProcessedDate, + SchedulerValidDate, } from '../models'; import { SchedulerState as State } from '../utils/SchedulerStore/SchedulerStore.types'; import { getWeekDayCode, serializeRRule } from '../utils/recurring-event-utils'; @@ -129,4 +131,18 @@ export const schedulerRecurringEventSelectors = { return serializeRRule(adapter, rruleA) === serializeRRule(adapter, rruleB); }, ), + /** + * Returns the 7 week days with code and date, starting at startOfWeek(visibleDate). + */ + weeklyDays: createSelectorMemoized( + (state: State) => state.adapter, + (state: State) => state.visibleDate, + (adapter, visibleDate): { code: RecurringEventWeekDayCode; date: SchedulerValidDate }[] => { + const start = adapter.startOfWeek(visibleDate); + return Array.from({ length: 7 }, (_, i) => { + const date = adapter.addDays(start, i); + return { code: getWeekDayCode(adapter, date), date }; + }); + }, + ), }; diff --git a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.css b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.css index 7e5ad4258233e..d4e9f9ed292b9 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.css +++ b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.css @@ -389,7 +389,6 @@ p { border: none; padding: 0; margin: 0; - font-size: var(--font-size-2); &[data-disabled] { color: var(--disabled-text-color); @@ -409,6 +408,7 @@ p { display: flex; align-items: center; gap: var(--space-2); + font-size: var(--font-size-2); } .EventPopoverAfterTimesInputWrapper { diff --git a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx index 144b266b7b79f..b434543c33095 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx @@ -119,6 +119,30 @@ export function FormContent(props: FormContentProps) { setControlled(newState); }; + const sanitizeRRule = (rule: RecurringEventRecurrenceRule): RecurringEventRecurrenceRule => { + const clean: RecurringEventRecurrenceRule = { ...rule }; + + switch (clean.freq) { + case 'DAILY': + delete clean.byDay; + delete clean.byMonthDay; + break; + case 'YEARLY': + delete clean.byDay; + delete clean.byMonthDay; + break; + case 'WEEKLY': + delete clean.byMonthDay; + break; + case 'MONTHLY': + delete clean.byDay; + break; + default: + break; + } + return clean; + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const { start, end } = computeRange(adapter, controlled); @@ -143,9 +167,7 @@ export function FormContent(props: FormContentProps) { if (controlled.recurrenceSelection === null) { rruleToSubmit = undefined; } else if (controlled.recurrenceSelection === 'custom') { - rruleToSubmit = { - ...controlled.rruleDraft, - }; + rruleToSubmit = sanitizeRRule(controlled.rruleDraft); } else { rruleToSubmit = recurrencePresets[controlled.recurrenceSelection]; } diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index ddde836c5e3af..2fd0184588a80 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -9,14 +9,20 @@ import { RadioGroup } from '@base-ui-components/react/radio-group'; import { Radio } from '@base-ui-components/react/radio'; import { Separator } from '@base-ui-components/react/separator'; import { ChevronDown } from 'lucide-react'; +import { Toggle } from '@base-ui-components/react/toggle'; +import { ToggleGroup } from '@base-ui-components/react/toggle-group'; import { SchedulerEventOccurrence, RecurringEventFrequency, RecurringEventPresetKey, + RecurringEventWeekDayCode, } from '@mui/x-scheduler-headless/models'; import { useSchedulerStoreContext } from '@mui/x-scheduler-headless/use-scheduler-store-context'; import { useAdapter } from '@mui/x-scheduler-headless/use-adapter'; -import { schedulerEventSelectors } from '@mui/x-scheduler-headless/scheduler-selectors'; +import { + schedulerEventSelectors, + schedulerRecurringEventSelectors, +} from '@mui/x-scheduler-headless/scheduler-selectors'; import { Tabs } from '@base-ui-components/react/tabs'; import { useTranslations } from '../../utils/TranslationsContext'; import { ControlledValue, EndsSelection, getEndsSelectionFromRRule } from './utils'; @@ -133,6 +139,19 @@ export function RecurrenceTab(props: RecurrenceTabProps) { })); }; + const handleChangeWeeklyDays = React.useCallback( + (next: string[] | RecurringEventWeekDayCode[]) => { + setControlled((prev) => ({ + ...prev, + rruleDraft: { + ...prev.rruleDraft, + byDay: next as RecurringEventWeekDayCode[], + }, + })); + }, + [setControlled], + ); + const customEndsValue: 'never' | 'after' | 'until' = getEndsSelectionFromRRule( controlled.rruleDraft, ); @@ -183,6 +202,18 @@ export function RecurrenceTab(props: RecurrenceTabProps) { }, ]; + const weeklyDays = useStore(store, schedulerRecurringEventSelectors.weeklyDays); + + const weeklyDayItems = React.useMemo( + () => + weeklyDays.map(({ code, date }) => ({ + code, + ariaLabel: adapter.format(date, 'weekday'), + text: adapter.format(date, 'weekdayShort'), + })), + [adapter, weeklyDays], + ); + return (
@@ -275,7 +306,21 @@ export function RecurrenceTab(props: RecurrenceTabProps) { {controlled.recurrenceSelection === 'custom' && controlled.rruleDraft.freq === 'WEEKLY' && ( -

TODO: Weekly Fields

+ + {translations.recurrenceWeeklyDaysLabel} + + {weeklyDayItems.map(({ code, ariaLabel, text }) => ( + + {text} + + ))} + + )} {controlled.recurrenceSelection === 'custom' && controlled.rruleDraft.freq === 'MONTHLY' && ( diff --git a/packages/x-scheduler/src/models/translations.ts b/packages/x-scheduler/src/models/translations.ts index 946ea54119a9e..6b940fa3ef08c 100644 --- a/packages/x-scheduler/src/models/translations.ts +++ b/packages/x-scheduler/src/models/translations.ts @@ -75,6 +75,7 @@ export interface SchedulerTranslations { recurrenceWeeklyPresetLabel: (weekday: string) => string; recurrenceMonthlyFrequencyLabel: string; recurrenceMonthlyPresetLabel: (dayNumber: number) => string; + recurrenceWeeklyDaysLabel: string; recurrenceYearlyFrequencyLabel: string; recurrenceYearlyPresetLabel: (date: string) => string; resourceLabel: string; diff --git a/packages/x-scheduler/src/styles/components/menus.css b/packages/x-scheduler/src/styles/components/menus.css index c23839e1c0fbc..724627160aad7 100644 --- a/packages/x-scheduler/src/styles/components/menus.css +++ b/packages/x-scheduler/src/styles/components/menus.css @@ -139,3 +139,30 @@ text-transform: uppercase; } } + +/* ToggleGroup */ +.mui-x-scheduler .ToggleGroup { + display: flex; + width: fit-content; + gap: var(--space-1); + padding: var(--space-1); + border: 1px solid var(--border-color); + border-radius: var(--radius-3); +} + +.mui-x-scheduler .ToggleItem { + background-color: transparent; + padding: var(--space-1) var(--space-2); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-2); + line-height: var(--line-height-1); + color: var(--secondary-text-color); + border-radius: var(--radius-3); +} + +.mui-x-scheduler .ToggleItem[data-pressed] { + background-color: var(--primary-3); + color: var(--primary-12); +} diff --git a/packages/x-scheduler/src/translations/enUS.ts b/packages/x-scheduler/src/translations/enUS.ts index 1532e46f7a1c8..49c653a80f9c6 100644 --- a/packages/x-scheduler/src/translations/enUS.ts +++ b/packages/x-scheduler/src/translations/enUS.ts @@ -75,6 +75,7 @@ export const enUS: SchedulerTranslations = { recurrenceWeeklyPresetLabel: (weekday) => `Repeats weekly on ${weekday}`, recurrenceMonthlyFrequencyLabel: 'months', recurrenceMonthlyPresetLabel: (dayNumber) => `Repeats monthly on day ${dayNumber}`, + recurrenceWeeklyDaysLabel: 'On', recurrenceYearlyFrequencyLabel: 'years', recurrenceYearlyPresetLabel: (date) => `Repeats annually on ${date}`, resourceLabel: 'Resource', From 7f4a23139b9066265d2e6c775958f7a32cb95100 Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 12:22:44 +0100 Subject: [PATCH 02/13] remove separator --- .../src/internals/components/event-popover/RecurrenceTab.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index 2fd0184588a80..a4806e83eecb4 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -327,8 +327,6 @@ export function RecurrenceTab(props: RecurrenceTabProps) {

TODO: Monthly Fields

)} - - Date: Thu, 13 Nov 2025 18:05:33 +0100 Subject: [PATCH 03/13] add monthly options --- .../schedulerRecurringEventSelectors.ts | 30 +++++- .../components/event-popover/FormContent.tsx | 15 ++- .../event-popover/RecurrenceTab.tsx | 101 ++++++++++++++---- .../x-scheduler/src/models/translations.ts | 7 +- packages/x-scheduler/src/translations/enUS.ts | 7 +- 5 files changed, 127 insertions(+), 33 deletions(-) diff --git a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts index b236ce814f876..c240e4444c69c 100644 --- a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts +++ b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts @@ -7,7 +7,11 @@ import { SchedulerValidDate, } from '../models'; import { SchedulerState as State } from '../utils/SchedulerStore/SchedulerStore.types'; -import { getWeekDayCode, serializeRRule } from '../utils/recurring-event-utils'; +import { + computeMonthlyOrdinal, + getWeekDayCode, + serializeRRule, +} from '../utils/recurring-event-utils'; export const schedulerRecurringEventSelectors = { /** @@ -145,4 +149,28 @@ export const schedulerRecurringEventSelectors = { }); }, ), + + /** + * Returns month reference for the given occurrence: dayOfMonth, weekday code and ordinal. + */ + monthlyReference: createSelectorMemoized( + (state: State) => state.adapter, + ( + adapter, + date: SchedulerProcessedDate, + ): { + dayOfMonth: number; + code: RecurringEventWeekDayCode; + ord: number; + date: SchedulerValidDate; + } => { + const d = adapter.startOfDay(date.value); + return { + dayOfMonth: adapter.getDate(d), + code: getWeekDayCode(adapter, d), + ord: computeMonthlyOrdinal(adapter, d), + date: d, + }; + }, + ), }; diff --git a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx index b434543c33095..7d6fa0459fcb4 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx @@ -124,18 +124,15 @@ export function FormContent(props: FormContentProps) { switch (clean.freq) { case 'DAILY': - delete clean.byDay; - delete clean.byMonthDay; - break; - case 'YEARLY': - delete clean.byDay; - delete clean.byMonthDay; + clean.byDay = []; + clean.byMonthDay = []; break; case 'WEEKLY': - delete clean.byMonthDay; + clean.byMonthDay = []; break; - case 'MONTHLY': - delete clean.byDay; + case 'YEARLY': + clean.byDay = []; + clean.byMonthDay = []; break; default: break; diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index a4806e83eecb4..553d2a22e121b 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -7,7 +7,6 @@ import { Input } from '@base-ui-components/react/input'; import { Select } from '@base-ui-components/react/select'; import { RadioGroup } from '@base-ui-components/react/radio-group'; import { Radio } from '@base-ui-components/react/radio'; -import { Separator } from '@base-ui-components/react/separator'; import { ChevronDown } from 'lucide-react'; import { Toggle } from '@base-ui-components/react/toggle'; import { ToggleGroup } from '@base-ui-components/react/toggle-group'; @@ -15,6 +14,7 @@ import { SchedulerEventOccurrence, RecurringEventFrequency, RecurringEventPresetKey, + RecurringEventByDayValue, RecurringEventWeekDayCode, } from '@mui/x-scheduler-headless/models'; import { useSchedulerStoreContext } from '@mui/x-scheduler-headless/use-scheduler-store-context'; @@ -48,6 +48,11 @@ export function RecurrenceTab(props: RecurrenceTabProps) { occurrence.id, ); const customDisabled = controlled.recurrenceSelection !== 'custom' || isPropertyReadOnly('rrule'); + const monthlyRef = useStore( + store, + schedulerRecurringEventSelectors.monthlyReference, + occurrence.start, + ); const handleRecurrenceSelectionChange = (value: RecurringEventPresetKey | null | 'custom') => { if (value === 'custom') { @@ -139,18 +144,29 @@ export function RecurrenceTab(props: RecurrenceTabProps) { })); }; - const handleChangeWeeklyDays = React.useCallback( - (next: string[] | RecurringEventWeekDayCode[]) => { - setControlled((prev) => ({ - ...prev, - rruleDraft: { - ...prev.rruleDraft, - byDay: next as RecurringEventWeekDayCode[], - }, - })); - }, - [setControlled], - ); + const handleChangeWeeklyDays = (next: RecurringEventWeekDayCode[]) => { + setControlled((prev) => ({ + ...prev, + rruleDraft: { + ...prev.rruleDraft, + byDay: next, + }, + })); + }; + + const handleChangeMonthlyGroup = (next: string[]) => { + const nextKey = next[0]; + + setControlled((prev) => { + if (nextKey === 'byDay') { + const value = `${monthlyRef.ord}${monthlyRef.code}` as RecurringEventByDayValue; + const { byMonthDay, ...rest } = prev.rruleDraft; + return { ...prev, rruleDraft: { ...rest, byDay: [value] } }; + } + const { byDay, ...rest } = prev.rruleDraft; + return { ...prev, rruleDraft: { ...rest, byMonthDay: [monthlyRef.dayOfMonth] } }; + }); + }; const customEndsValue: 'never' | 'after' | 'until' = getEndsSelectionFromRRule( controlled.rruleDraft, @@ -207,13 +223,43 @@ export function RecurrenceTab(props: RecurrenceTabProps) { const weeklyDayItems = React.useMemo( () => weeklyDays.map(({ code, date }) => ({ - code, + value: code, ariaLabel: adapter.format(date, 'weekday'), - text: adapter.format(date, 'weekdayShort'), + label: adapter.format(date, 'weekdayShort'), })), [adapter, weeklyDays], ); + const monthlyItems = React.useMemo(() => { + const ord = monthlyRef.ord; + const dayOfMonth = translations.recurrenceMonthlyDayOfMonthLabel?.(monthlyRef.dayOfMonth); + const isLast = ord === -1; + const weekdayShort = adapter.formatByString(monthlyRef.date, 'ccc'); + const weekAriaLabel = isLast + ? translations.recurrenceMonthlyLastWeekLabel(weekday) + : translations.recurrenceMonthlyWeekNumberLabel?.(ord, weekday); + const weekText = isLast + ? translations.recurrenceMonthlyLastWeekShort(weekdayShort) + : translations.recurrenceMonthlyWeekNumberShort?.(ord, weekdayShort); + + return [ + { + value: 'byMonthDay', + ariaLabel: `${dayOfMonth}`, + label: dayOfMonth, + }, + { + value: 'byDay', + ariaLabel: weekAriaLabel, + label: weekText, + }, + ]; + }, [adapter, monthlyRef.date, monthlyRef.dayOfMonth, monthlyRef.ord, translations, weekday]); + + const monthlyMode: 'byMonthDay' | 'byDay' = controlled.rruleDraft.byDay?.length + ? 'byDay' + : 'byMonthDay'; + return (
@@ -306,17 +352,17 @@ export function RecurrenceTab(props: RecurrenceTabProps) { {controlled.recurrenceSelection === 'custom' && controlled.rruleDraft.freq === 'WEEKLY' && ( - - {translations.recurrenceWeeklyDaysLabel} + + {translations.recurrenceWeeklyMonthlySpecificInputsLabel} - {weeklyDayItems.map(({ code, ariaLabel, text }) => ( - - {text} + {weeklyDayItems.map(({ value, ariaLabel, label }) => ( + + {label} ))} @@ -324,7 +370,20 @@ export function RecurrenceTab(props: RecurrenceTabProps) { )} {controlled.recurrenceSelection === 'custom' && controlled.rruleDraft.freq === 'MONTHLY' && ( -

TODO: Monthly Fields

+ + {translations.recurrenceWeeklyMonthlySpecificInputsLabel} + + {monthlyItems.map(({ value, ariaLabel, label }) => ( + + {label} + + ))} + + )} string; + recurrenceMonthlyDayOfMonthLabel: (dayNumber: number) => string; recurrenceMonthlyFrequencyLabel: string; + recurrenceMonthlyLastWeekLabel: (weekDay: string) => string; + recurrenceMonthlyLastWeekShort: (weekDay: string) => string; recurrenceMonthlyPresetLabel: (dayNumber: number) => string; - recurrenceWeeklyDaysLabel: string; + recurrenceMonthlyWeekNumberLabel: (ord: number, weekDay: string) => string; + recurrenceMonthlyWeekNumberShort: (ord: number, weekDay: string) => string; + recurrenceWeeklyMonthlySpecificInputsLabel: string; recurrenceYearlyFrequencyLabel: string; recurrenceYearlyPresetLabel: (date: string) => string; resourceLabel: string; diff --git a/packages/x-scheduler/src/translations/enUS.ts b/packages/x-scheduler/src/translations/enUS.ts index 49c653a80f9c6..8479b087ae7b1 100644 --- a/packages/x-scheduler/src/translations/enUS.ts +++ b/packages/x-scheduler/src/translations/enUS.ts @@ -74,8 +74,13 @@ export const enUS: SchedulerTranslations = { recurrenceWeeklyFrequencyLabel: 'weeks', recurrenceWeeklyPresetLabel: (weekday) => `Repeats weekly on ${weekday}`, recurrenceMonthlyFrequencyLabel: 'months', + recurrenceMonthlyDayOfMonthLabel: (dayNumber) => `Day ${dayNumber}`, + recurrenceMonthlyLastWeekLabel: (weekDay) => `${weekDay} the last week of the month`, + recurrenceMonthlyLastWeekShort: (weekDay) => `${weekDay} last week`, recurrenceMonthlyPresetLabel: (dayNumber) => `Repeats monthly on day ${dayNumber}`, - recurrenceWeeklyDaysLabel: 'On', + recurrenceMonthlyWeekNumberLabel: (ord, weekDay) => `${weekDay} week ${ord} of the month`, + recurrenceMonthlyWeekNumberShort: (ord, weekDay) => `${weekDay} week ${ord}`, + recurrenceWeeklyMonthlySpecificInputsLabel: 'On', recurrenceYearlyFrequencyLabel: 'years', recurrenceYearlyPresetLabel: (date) => `Repeats annually on ${date}`, resourceLabel: 'Resource', From 674f70f6474a1dff828c5164cb2c680e4d7a7375 Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 18:09:03 +0100 Subject: [PATCH 04/13] fix --- .../src/internals/components/event-popover/RecurrenceTab.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index 553d2a22e121b..8473048fe3ab5 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -312,7 +312,6 @@ export function RecurrenceTab(props: RecurrenceTabProps) { {translations.recurrenceEveryLabel}
Date: Thu, 13 Nov 2025 18:43:30 +0100 Subject: [PATCH 05/13] add tests --- .../event-popover/EventPopover.test.tsx | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx index 34a07fcda25c4..98bdd60fc36d0 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx @@ -942,6 +942,124 @@ describe('', () => { adapter.startOfDay(adapter.date('2025-07-20T00:00:00')), ); }); + + it('should submit custom weekly with selected weekdays', async () => { + const onEventsChange = spy(); + + const { user } = render( + + + + + , + ); + + await user.click(screen.getByRole('tab', { name: /recurrence/i })); + await user.click(screen.getByRole('combobox', { name: /recurrence/i })); + await user.click(await screen.findByRole('option', { name: /custom/i })); + + const repeatGroup = screen.getByRole('group', { name: /repeat/i }); + const freqCombo = within(repeatGroup).getByRole('combobox'); + await user.click(freqCombo); + await user.click(await screen.findByRole('option', { name: /weeks/i })); + + // Select Monday and Friday in the weekly day toggles + await user.click(screen.getByRole('button', { name: /monday/i })); + await user.click(screen.getByRole('button', { name: /friday/i })); + + await user.click(screen.getByRole('button', { name: /save changes/i })); + + expect(onEventsChange.calledOnce).to.equal(true); + const updated = onEventsChange.firstCall.firstArg[0]; + + expect(updated.rrule).to.deep.equal({ + freq: 'WEEKLY', + interval: 1, + byDay: ['MO', 'FR'], + byMonthDay: [], + }); + }); + + it('should submit custom monthly with "day of month" option', async () => { + const onEventsChange = spy(); + + const { user } = render( + + + + + , + ); + + await user.click(screen.getByRole('tab', { name: /recurrence/i })); + await user.click(screen.getByRole('combobox', { name: /recurrence/i })); + await user.click(await screen.findByRole('option', { name: /custom/i })); + + const repeatGroup = screen.getByRole('group', { name: /repeat/i }); + const freqCombo = within(repeatGroup).getByRole('combobox'); + await user.click(freqCombo); + await user.click(await screen.findByRole('option', { name: /months/i })); + + await user.click(screen.getByRole('button', { name: /day 26/i })); // DEFAULT_EVENT is 2025-05-26 + + await user.click(screen.getByRole('button', { name: /save changes/i })); + + expect(onEventsChange.calledOnce).to.equal(true); + const updated = onEventsChange.firstCall.firstArg[0]; + + expect(updated.rrule).to.deep.equal({ + freq: 'MONTHLY', + interval: 1, + byMonthDay: [26], + }); + }); + + it('should submit custom monthly with "ordinal weekday" option', async () => { + const onEventsChange = spy(); + + const { user } = render( + + + + + , + ); + + await user.click(screen.getByRole('tab', { name: /recurrence/i })); + await user.click(screen.getByRole('combobox', { name: /recurrence/i })); + await user.click(await screen.findByRole('option', { name: /custom/i })); + + const repeatGroup = screen.getByRole('group', { name: /repeat/i }); + const freqCombo = within(repeatGroup).getByRole('combobox'); + await user.click(freqCombo); + await user.click(await screen.findByRole('option', { name: /months/i })); + + // The DEFAULT_EVENT (2025-05-26 Mon) is the last Monday of the month ("-1MO") + await user.click(screen.getByRole('button', { name: /mon.*last week/i })); + + await user.click(screen.getByRole('button', { name: /save changes/i })); + + expect(onEventsChange.calledOnce).to.equal(true); + const updated = onEventsChange.firstCall.firstArg[0]; + + expect(updated.rrule).to.deep.equal({ + freq: 'MONTHLY', + interval: 1, + byDay: ['-1MO'], + }); + }); }); }); From af89c75e005f6464e53907438015967df6d360bb Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 18:51:35 +0100 Subject: [PATCH 06/13] fixes --- .../src/internals/components/event-popover/FormContent.tsx | 5 +---- packages/x-scheduler/src/translations/enUS.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx index 7d6fa0459fcb4..6ecd2088585f6 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx @@ -124,16 +124,13 @@ export function FormContent(props: FormContentProps) { switch (clean.freq) { case 'DAILY': + case 'YEARLY': clean.byDay = []; clean.byMonthDay = []; break; case 'WEEKLY': clean.byMonthDay = []; break; - case 'YEARLY': - clean.byDay = []; - clean.byMonthDay = []; - break; default: break; } diff --git a/packages/x-scheduler/src/translations/enUS.ts b/packages/x-scheduler/src/translations/enUS.ts index 8479b087ae7b1..ca7405bf41244 100644 --- a/packages/x-scheduler/src/translations/enUS.ts +++ b/packages/x-scheduler/src/translations/enUS.ts @@ -75,7 +75,7 @@ export const enUS: SchedulerTranslations = { recurrenceWeeklyPresetLabel: (weekday) => `Repeats weekly on ${weekday}`, recurrenceMonthlyFrequencyLabel: 'months', recurrenceMonthlyDayOfMonthLabel: (dayNumber) => `Day ${dayNumber}`, - recurrenceMonthlyLastWeekLabel: (weekDay) => `${weekDay} the last week of the month`, + recurrenceMonthlyLastWeekLabel: (weekDay) => `${weekDay} of the last week of the month`, recurrenceMonthlyLastWeekShort: (weekDay) => `${weekDay} last week`, recurrenceMonthlyPresetLabel: (dayNumber) => `Repeats monthly on day ${dayNumber}`, recurrenceMonthlyWeekNumberLabel: (ord, weekDay) => `${weekDay} week ${ord} of the month`, From f226c77c12e0dfb665e701fbca80633ed08345cc Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 19:00:42 +0100 Subject: [PATCH 07/13] refactor --- .../components/event-popover/FormContent.tsx | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx index 6ecd2088585f6..d3a1e8444a124 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx @@ -119,24 +119,6 @@ export function FormContent(props: FormContentProps) { setControlled(newState); }; - const sanitizeRRule = (rule: RecurringEventRecurrenceRule): RecurringEventRecurrenceRule => { - const clean: RecurringEventRecurrenceRule = { ...rule }; - - switch (clean.freq) { - case 'DAILY': - case 'YEARLY': - clean.byDay = []; - clean.byMonthDay = []; - break; - case 'WEEKLY': - clean.byMonthDay = []; - break; - default: - break; - } - return clean; - }; - const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const { start, end } = computeRange(adapter, controlled); @@ -161,7 +143,7 @@ export function FormContent(props: FormContentProps) { if (controlled.recurrenceSelection === null) { rruleToSubmit = undefined; } else if (controlled.recurrenceSelection === 'custom') { - rruleToSubmit = sanitizeRRule(controlled.rruleDraft); + rruleToSubmit = controlled.rruleDraft; } else { rruleToSubmit = recurrencePresets[controlled.recurrenceSelection]; } From 14d2d5f10cb0930a17955a01d6b141ad0750294c Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 19:02:05 +0100 Subject: [PATCH 08/13] refactor --- .../schedulerRecurringEventSelectors.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts index c240e4444c69c..ea0401a7b56ff 100644 --- a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts +++ b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerRecurringEventSelectors.ts @@ -164,12 +164,11 @@ export const schedulerRecurringEventSelectors = { ord: number; date: SchedulerValidDate; } => { - const d = adapter.startOfDay(date.value); return { - dayOfMonth: adapter.getDate(d), - code: getWeekDayCode(adapter, d), - ord: computeMonthlyOrdinal(adapter, d), - date: d, + dayOfMonth: adapter.getDate(date.value), + code: getWeekDayCode(adapter, date.value), + ord: computeMonthlyOrdinal(adapter, date.value), + date: date.value, }; }, ), From cb958718bdd7d777e2096634ae93bc0c6068fab7 Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 19:04:56 +0100 Subject: [PATCH 09/13] refactor --- .../components/event-popover/RecurrenceTab.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index 8473048fe3ab5..ac30ebf5d4811 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -232,26 +232,26 @@ export function RecurrenceTab(props: RecurrenceTabProps) { const monthlyItems = React.useMemo(() => { const ord = monthlyRef.ord; - const dayOfMonth = translations.recurrenceMonthlyDayOfMonthLabel?.(monthlyRef.dayOfMonth); + const dayOfMonthLabel = translations.recurrenceMonthlyDayOfMonthLabel?.(monthlyRef.dayOfMonth); const isLast = ord === -1; const weekdayShort = adapter.formatByString(monthlyRef.date, 'ccc'); const weekAriaLabel = isLast ? translations.recurrenceMonthlyLastWeekLabel(weekday) : translations.recurrenceMonthlyWeekNumberLabel?.(ord, weekday); - const weekText = isLast + const weekLabel = isLast ? translations.recurrenceMonthlyLastWeekShort(weekdayShort) : translations.recurrenceMonthlyWeekNumberShort?.(ord, weekdayShort); return [ { value: 'byMonthDay', - ariaLabel: `${dayOfMonth}`, - label: dayOfMonth, + ariaLabel: dayOfMonthLabel, + label: dayOfMonthLabel, }, { value: 'byDay', ariaLabel: weekAriaLabel, - label: weekText, + label: weekLabel, }, ]; }, [adapter, monthlyRef.date, monthlyRef.dayOfMonth, monthlyRef.ord, translations, weekday]); From 502503a910400bffd7fe09f4f6fd7e7264aecd8a Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 19:05:44 +0100 Subject: [PATCH 10/13] refactor --- .../internals/components/event-popover/RecurrenceTab.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index ac30ebf5d4811..40bbdf1f68087 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -231,16 +231,16 @@ export function RecurrenceTab(props: RecurrenceTabProps) { ); const monthlyItems = React.useMemo(() => { - const ord = monthlyRef.ord; + const ordinal = monthlyRef.ord; const dayOfMonthLabel = translations.recurrenceMonthlyDayOfMonthLabel?.(monthlyRef.dayOfMonth); - const isLast = ord === -1; + const isLast = ordinal === -1; const weekdayShort = adapter.formatByString(monthlyRef.date, 'ccc'); const weekAriaLabel = isLast ? translations.recurrenceMonthlyLastWeekLabel(weekday) - : translations.recurrenceMonthlyWeekNumberLabel?.(ord, weekday); + : translations.recurrenceMonthlyWeekNumberLabel?.(ordinal, weekday); const weekLabel = isLast ? translations.recurrenceMonthlyLastWeekShort(weekdayShort) - : translations.recurrenceMonthlyWeekNumberShort?.(ord, weekdayShort); + : translations.recurrenceMonthlyWeekNumberShort?.(ordinal, weekdayShort); return [ { From d9fd55ee6172dd392c8497d28d6bbe0f19db1219 Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Thu, 13 Nov 2025 19:10:50 +0100 Subject: [PATCH 11/13] refactor --- .../internals/components/event-popover/RecurrenceTab.tsx | 8 ++++---- packages/x-scheduler/src/models/translations.ts | 4 ++-- packages/x-scheduler/src/translations/enUS.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index 40bbdf1f68087..c194270209847 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -236,11 +236,11 @@ export function RecurrenceTab(props: RecurrenceTabProps) { const isLast = ordinal === -1; const weekdayShort = adapter.formatByString(monthlyRef.date, 'ccc'); const weekAriaLabel = isLast - ? translations.recurrenceMonthlyLastWeekLabel(weekday) - : translations.recurrenceMonthlyWeekNumberLabel?.(ordinal, weekday); + ? translations.recurrenceMonthlyLastWeekAriaLabel(weekday) + : translations.recurrenceMonthlyWeekNumberAriaLabel?.(ordinal, weekday); const weekLabel = isLast - ? translations.recurrenceMonthlyLastWeekShort(weekdayShort) - : translations.recurrenceMonthlyWeekNumberShort?.(ordinal, weekdayShort); + ? translations.recurrenceMonthlyLastWeekLabel(weekdayShort) + : translations.recurrenceMonthlyWeekNumberLabel?.(ordinal, weekdayShort); return [ { diff --git a/packages/x-scheduler/src/models/translations.ts b/packages/x-scheduler/src/models/translations.ts index 9a164ecb6dc48..9d3a83ef0b14d 100644 --- a/packages/x-scheduler/src/models/translations.ts +++ b/packages/x-scheduler/src/models/translations.ts @@ -75,11 +75,11 @@ export interface SchedulerTranslations { recurrenceWeeklyPresetLabel: (weekday: string) => string; recurrenceMonthlyDayOfMonthLabel: (dayNumber: number) => string; recurrenceMonthlyFrequencyLabel: string; + recurrenceMonthlyLastWeekAriaLabel: (weekDay: string) => string; recurrenceMonthlyLastWeekLabel: (weekDay: string) => string; - recurrenceMonthlyLastWeekShort: (weekDay: string) => string; recurrenceMonthlyPresetLabel: (dayNumber: number) => string; + recurrenceMonthlyWeekNumberAriaLabel: (ord: number, weekDay: string) => string; recurrenceMonthlyWeekNumberLabel: (ord: number, weekDay: string) => string; - recurrenceMonthlyWeekNumberShort: (ord: number, weekDay: string) => string; recurrenceWeeklyMonthlySpecificInputsLabel: string; recurrenceYearlyFrequencyLabel: string; recurrenceYearlyPresetLabel: (date: string) => string; diff --git a/packages/x-scheduler/src/translations/enUS.ts b/packages/x-scheduler/src/translations/enUS.ts index ca7405bf41244..a0dab15c7050a 100644 --- a/packages/x-scheduler/src/translations/enUS.ts +++ b/packages/x-scheduler/src/translations/enUS.ts @@ -75,11 +75,11 @@ export const enUS: SchedulerTranslations = { recurrenceWeeklyPresetLabel: (weekday) => `Repeats weekly on ${weekday}`, recurrenceMonthlyFrequencyLabel: 'months', recurrenceMonthlyDayOfMonthLabel: (dayNumber) => `Day ${dayNumber}`, - recurrenceMonthlyLastWeekLabel: (weekDay) => `${weekDay} of the last week of the month`, - recurrenceMonthlyLastWeekShort: (weekDay) => `${weekDay} last week`, + recurrenceMonthlyLastWeekAriaLabel: (weekDay) => `${weekDay} of the last week of the month`, + recurrenceMonthlyLastWeekLabel: (weekDay) => `${weekDay} last week`, recurrenceMonthlyPresetLabel: (dayNumber) => `Repeats monthly on day ${dayNumber}`, - recurrenceMonthlyWeekNumberLabel: (ord, weekDay) => `${weekDay} week ${ord} of the month`, - recurrenceMonthlyWeekNumberShort: (ord, weekDay) => `${weekDay} week ${ord}`, + recurrenceMonthlyWeekNumberAriaLabel: (ord, weekDay) => `${weekDay} week ${ord} of the month`, + recurrenceMonthlyWeekNumberLabel: (ord, weekDay) => `${weekDay} week ${ord}`, recurrenceWeeklyMonthlySpecificInputsLabel: 'On', recurrenceYearlyFrequencyLabel: 'years', recurrenceYearlyPresetLabel: (date) => `Repeats annually on ${date}`, From 771498da72342fb5dd91a148493389f6dac43ceb Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Fri, 14 Nov 2025 09:35:47 +0100 Subject: [PATCH 12/13] refactor --- .../src/internals/components/event-popover/RecurrenceTab.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx index c194270209847..8e5b3942b8363 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/RecurrenceTab.tsx @@ -53,6 +53,7 @@ export function RecurrenceTab(props: RecurrenceTabProps) { schedulerRecurringEventSelectors.monthlyReference, occurrence.start, ); + const weeklyDays = useStore(store, schedulerRecurringEventSelectors.weeklyDays); const handleRecurrenceSelectionChange = (value: RecurringEventPresetKey | null | 'custom') => { if (value === 'custom') { @@ -218,8 +219,6 @@ export function RecurrenceTab(props: RecurrenceTabProps) { }, ]; - const weeklyDays = useStore(store, schedulerRecurringEventSelectors.weeklyDays); - const weeklyDayItems = React.useMemo( () => weeklyDays.map(({ code, date }) => ({ From 7b64c0062dcc54b5ba084773eca928294bd218cd Mon Sep 17 00:00:00 2001 From: Rita Iglesias Gandara Date: Fri, 14 Nov 2025 12:23:16 +0100 Subject: [PATCH 13/13] Trigger pipeline