Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/calendar-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue in Custom view where the "Header day format" was only applied to the toolbar title and not to the day, week, and month column headers.

## [2.4.0] - 2026-03-20

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { render, screen } from "@testing-library/react";
import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils";

import MxCalendar from "../Calendar";
import { CalendarContainerProps } from "../../typings/CalendarProps";
import MxCalendar from "../Calendar";
import { CalendarPropsBuilder } from "../helpers/CalendarPropsBuilder";

// Mock react-big-calendar to avoid View.title issues
Expand Down Expand Up @@ -256,3 +256,79 @@ describe("CalendarPropsBuilder validation", () => {
expect(result.timeslots).toBe(2);
});
});

describe("CalendarPropsBuilder column header formats", () => {
const buildFormats = (toolbarItems: CalendarContainerProps["toolbarItems"]): any => {
const localizer = {
format: jest.fn((_date: Date, pattern: string) => pattern),
parse: jest.fn(),
startOfWeek: jest.fn(),
getDay: jest.fn(),
messages: {}
} as any;
const builder = new CalendarPropsBuilder({ ...customViewProps, toolbarItems });
return { formats: builder.build(localizer, "en").formats, localizer };
};

const toolbarItem = (
itemType: string,
overrides: Partial<CalendarContainerProps["toolbarItems"][number]> = {}
): CalendarContainerProps["toolbarItems"][number] =>
({
itemType,
position: "left",
renderMode: "button",
buttonStyle: "default",
...overrides
}) as any;

it("wires 'Header day format' on a day item into RBC dayFormat (the column header)", () => {
const { formats, localizer } = buildFormats([
toolbarItem("day", { customViewHeaderDayFormat: dynamic("EE dd-MM") })
]);

expect(typeof formats.dayFormat).toBe("function");
formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer);
expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EE dd-MM", "en");
});

it("wires 'Header day format' on a month item into RBC weekdayFormat", () => {
const { formats, localizer } = buildFormats([
toolbarItem("month", { customViewHeaderDayFormat: dynamic("EEEE") })
]);

expect(typeof formats.weekdayFormat).toBe("function");
formats.weekdayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer);
expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EEEE", "en");
});

it("prefers the week pattern for the shared dayFormat when day and week both set it", () => {
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
const { formats, localizer } = buildFormats([
toolbarItem("day", { customViewHeaderDayFormat: dynamic("dd") }),
toolbarItem("week", { customViewHeaderDayFormat: dynamic("EE dd-MM") })
]);

formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer);
expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EE dd-MM", "en");
expect(warn).toHaveBeenCalledWith(expect.stringContaining("shares a single"));
warn.mockRestore();
});

it("leaves dayFormat/weekdayFormat unset when no header format is configured (RBC defaults preserved)", () => {
const { formats } = buildFormats([toolbarItem("day"), toolbarItem("month")]);

expect(formats.dayFormat).toBeUndefined();
expect(formats.weekdayFormat).toBeUndefined();
});

it("wires 'Time gutter format' on a day item into RBC timeGutterFormat", () => {
const { formats, localizer } = buildFormats([
toolbarItem("day", { customViewGutterTimeFormat: dynamic("HH:mm") })
]);

expect(typeof formats.timeGutterFormat).toBe("function");
formats.timeGutterFormat(new Date("2025-04-28T14:00:00Z"), "en", localizer);
expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "HH:mm", "en");
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ObjectItem } from "mendix";
import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar";
import { CustomWeekController } from "./CustomWeekController";
import { CalendarContainerProps } from "../../typings/CalendarProps";
import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar";
import { eventPropGetter, getTextValue } from "../utils/calendar-utils";
import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings";
import { CustomWeekController } from "./CustomWeekController";

export class CalendarPropsBuilder {
private visibleDays: Set<number>;
Expand Down Expand Up @@ -225,6 +225,34 @@ export class CalendarPropsBuilder {
loc.format(date, monthHeaderPattern, culture);
}

// Per-column headers — distinct from the toolbar title above.
// RBC renders the "07 Tue" day-column headers via `dayFormat` (week/day time-grid,
// TimeGridHeader.js) and the month weekday headers via `weekdayFormat` (Month.js).
// These are separate from dayHeaderFormat/monthHeaderFormat (the toolbar title), so we
// must set them explicitly or RBC's date-fns defaults ("dd eee") always win.
if (monthHeaderPattern) {
formats.weekdayFormat = (date: Date, culture: string, loc: DateLocalizer) =>
loc.format(date, monthHeaderPattern, culture);
}

// RBC exposes a single global `dayFormat` shared by the day AND week column headers, so
// when a day item and a week item each carry a different "Header day format" we can only
// honor one. Precedence matches `chosenTimeGutter` below (week → day → work_week) for
// consistency across the shared-key formats. weekHeaderPattern already folds in
// week/work_week; dayHeaderPattern is the "day" item.
const columnDayPattern: string | undefined = weekHeaderPattern || dayHeaderPattern;
if (weekHeaderPattern && dayHeaderPattern && weekHeaderPattern !== dayHeaderPattern) {
console.warn(
`[Calendar] Both week and day "Header day format" are set to different patterns ` +
`("${weekHeaderPattern}" vs "${dayHeaderPattern}"). react-big-calendar shares a single ` +
`column-header format, so "${columnDayPattern}" will be used for both views.`
);
}
if (columnDayPattern) {
formats.dayFormat = (date: Date, culture: string, loc: DateLocalizer) =>
loc.format(date, columnDayPattern, culture);
}

const agendaHeaderPattern = getPattern(byType.get("agenda")?.customViewHeaderDayFormat);
if (agendaHeaderPattern) {
formats.agendaHeaderFormat = (range: { start: Date; end: Date }, culture: string, loc: DateLocalizer) =>
Expand Down
Loading