diff --git a/packages/pluggableWidgets/calendar-web/CHANGELOG.md b/packages/pluggableWidgets/calendar-web/CHANGELOG.md
index 4bd6ccc4ac..a2538870ad 100644
--- a/packages/pluggableWidgets/calendar-web/CHANGELOG.md
+++ b/packages/pluggableWidgets/calendar-web/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
+### Fixed
+
+- Improved handling of the start date attribute to ensure correct calendar initialization.
+
## [2.3.0] - 2026-02-17
### Added
diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx
index a7d8d31d52..878bb94fb4 100644
--- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx
+++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx
@@ -1,4 +1,4 @@
-import { ReactElement, useMemo } from "react";
+import { Fragment, ReactElement, useMemo } from "react";
import classNames from "classnames";
import { CalendarContainerProps } from "../typings/CalendarProps";
import { CalendarPropsBuilder } from "./helpers/CalendarPropsBuilder";
@@ -26,9 +26,16 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement
}, [props, calendarController, localizer, culture]);
const calendarEvents = useCalendarEvents(props);
+
return (
-
-
-
+
+ {props.startDateAttribute?.status === "loading" ? (
+
+ ) : (
+
+
+
+ )}
+
);
}
diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml
index 0af2436b56..25e9204adf 100644
--- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml
+++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml
@@ -62,13 +62,6 @@
-
- Start date attribute
- The start date that should be shown in the view
-
-
-
-
@@ -139,6 +132,13 @@
Time slots
The number of slots per "section" in the time grid views. Adjust with step to change the default of 1 hour long groups, with 30 minute slots
+
+ Start date attribute
+ The DateTime attribute used on initial load
+
+
+
+
diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
index 5898ddd82e..6174878704 100644
--- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
+++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
@@ -1,4 +1,4 @@
-import { render } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils";
import MxCalendar from "../Calendar";
@@ -10,37 +10,39 @@ jest.mock("react-big-calendar", () => {
const originalModule = jest.requireActual("react-big-calendar");
return {
...originalModule,
- Calendar: ({
- children,
- defaultView,
- culture,
- resizable,
- selectable,
- showAllEvents,
- min,
- max,
- events,
- step,
- timeslots,
- ...domProps
- }: any) => (
-
- {children}
-
- ),
+ Calendar: (mockProps: any) => {
+ const {
+ children,
+ defaultView,
+ defaultDate,
+ culture,
+ resizable,
+ selectable,
+ showAllEvents,
+ events,
+ step,
+ timeslots,
+ ...domProps
+ } = mockProps;
+
+ return (
+
+ {children}
+
+ );
+ },
dateFnsLocalizer: () => ({
format: jest.fn(),
parse: jest.fn(),
@@ -58,7 +60,7 @@ jest.mock("react-big-calendar", () => {
});
jest.mock("react-big-calendar/lib/addons/dragAndDrop", () => {
- return jest.fn((Component: any) => Component);
+ return jest.fn(Component => Component);
});
const customViewProps: CalendarContainerProps = {
@@ -97,11 +99,6 @@ const customViewProps: CalendarContainerProps = {
topBarDateFormat: undefined
};
-const standardViewProps: CalendarContainerProps = {
- ...customViewProps,
- view: "standard"
-};
-
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2025-04-28T12:00:00Z"));
@@ -123,19 +120,78 @@ describe("Calendar", () => {
expect(container.querySelector(".calendar-class")).toBeTruthy();
});
- it("does not render custom view button in standard view", () => {
- const { container } = render();
- expect(container).toBeTruthy();
- // Since we're mocking the calendar, we can't test for specific text content
- // but we can verify the component renders without errors
- });
-
it("passes step and timeslots to the calendar", () => {
const { getByTestId } = render();
const calendar = getByTestId("mock-calendar");
expect(calendar.getAttribute("data-step")).toBe("60");
expect(calendar.getAttribute("data-timeslots")).toBe("2");
});
+
+ it("renders loading bar when startDateAttribute is loading", () => {
+ const props = {
+ ...customViewProps,
+ startDateAttribute: {
+ status: "loading"
+ } as any
+ };
+
+ const { container } = render();
+
+ expect(container.querySelector(".widget-calendar-loading-bar")).toBeTruthy();
+ expect(container.querySelector("progress.widget-calendar-loading-bar")).toBeTruthy();
+ expect(screen.queryByTestId("mock-calendar")).toBeFalsy();
+ });
+
+ it("renders calendar when startDateAttribute is available", () => {
+ const props = {
+ ...customViewProps,
+ startDateAttribute: {
+ status: "available",
+ value: new Date("2025-05-01T00:00:00.000Z")
+ } as any
+ };
+
+ render();
+
+ expect(screen.getByTestId("mock-calendar")).toBeTruthy();
+ expect(screen.queryByRole("progressbar")).toBeFalsy();
+ });
+
+ it("renders calendar when startDateAttribute is unavailable", () => {
+ const props = {
+ ...customViewProps,
+ startDateAttribute: {
+ status: "unavailable"
+ } as any
+ };
+
+ render();
+
+ expect(screen.getByTestId("mock-calendar")).toBeTruthy();
+ expect(screen.queryByRole("progressbar")).toBeFalsy();
+ });
+
+ it("renders calendar when startDateAttribute is undefined", () => {
+ render();
+
+ expect(screen.getByTestId("mock-calendar")).toBeTruthy();
+ expect(screen.queryByRole("progressbar")).toBeFalsy();
+ });
+
+ it("passes defaultDate from startDateAttribute value", () => {
+ const defaultDate = new Date("2025-06-10T08:30:00.000Z");
+ const props = {
+ ...customViewProps,
+ startDateAttribute: {
+ status: "available",
+ value: defaultDate
+ } as any
+ };
+
+ render();
+
+ expect(screen.getByTestId("mock-calendar").getAttribute("data-default-date")).toBe("2025-06-10T08:30:00.000Z");
+ });
});
describe("CalendarPropsBuilder validation", () => {
@@ -147,7 +203,10 @@ describe("CalendarPropsBuilder validation", () => {
messages: {}
} as any;
- const buildWithStepTimeslots = (step: number, timeslots: number) => {
+ const buildWithStepTimeslots = (
+ step: number,
+ timeslots: number
+ ): ReturnType => {
const props = { ...customViewProps, step, timeslots };
const builder = new CalendarPropsBuilder(props);
return builder.build(mockLocalizer, "en");
diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap b/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap
index 53096ea284..941e99e4ec 100644
--- a/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap
+++ b/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap
@@ -10,8 +10,6 @@ exports[`Calendar renders correctly with basic props 1`] = `
data-culture="en-US"
data-default-view="day"
data-events-count="0"
- data-max="2025-04-28T23:59:59.000Z"
- data-min="2025-04-28T00:00:00.000Z"
data-resizable="true"
data-selectable="true"
data-show-all-events="true"
@@ -20,7 +18,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
data-timeslots="2"
formats="[object Object]"
localizer="[object Object]"
+ max="Mon Apr 28 2025 23:59:59 GMT+0000 (Coordinated Universal Time)"
messages="[object Object]"
+ min="Mon Apr 28 2025 00:00:00 GMT+0000 (Coordinated Universal Time)"
views="[object Object]"
/>
diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts
index 6d3c119b8e..d6176e11a6 100644
--- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts
+++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts
@@ -16,6 +16,7 @@ export class CalendarPropsBuilder {
private toolbarItems?: ResolvedToolbarItem[];
private step: number;
private timeSlots: number;
+ private defaultDate?: Date;
constructor(private props: CalendarContainerProps) {
this.isCustomView = props.view === "custom";
@@ -36,6 +37,7 @@ export class CalendarPropsBuilder {
`[Calendar] timeslots value ${props.timeslots} was clamped to ${this.timeSlots}. Must be between 1 and 4.`
);
}
+ this.defaultDate = props.startDateAttribute?.value;
}
updateProps(props: CalendarContainerProps): void {
@@ -43,6 +45,7 @@ export class CalendarPropsBuilder {
this.props = props;
this.events = this.buildEvents(props.databaseDataSource?.items ?? []);
this.toolbarItems = this.buildToolbarItems();
+ this.defaultDate = props.startDateAttribute?.value;
}
build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps {
@@ -86,7 +89,8 @@ export class CalendarPropsBuilder {
min: this.minTime,
max: this.maxTime,
step: this.step,
- timeslots: this.timeSlots
+ timeslots: this.timeSlots,
+ ...(this.defaultDate ? { defaultDate: this.defaultDate } : {})
};
}
diff --git a/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss b/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss
index d324b682cd..72181b73be 100644
--- a/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss
+++ b/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss
@@ -1,4 +1,5 @@
@use "sass:color";
+$brand-primary: #264ae5 !default;
.widget-calendar {
$cal-form-group-margin-bottom: 15px !default;
@@ -79,4 +80,75 @@
.rbc-event {
background-color: var(--brand-primary, $cal-brand-primary);
}
+
+ &-loading-bar {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: var(--border-color-default, #ced0d3);
+ border: none;
+ border-radius: 2px;
+ color: var(--brand-primary, $brand-primary);
+ height: 4px;
+ width: 100%;
+
+ &::-webkit-progress-bar {
+ background-color: transparent;
+ }
+
+ &::-webkit-progress-value {
+ background-color: currentColor;
+ transition: all 0.2s;
+ }
+
+ &::-moz-progress-bar {
+ background-color: currentColor;
+ transition: all 0.2s;
+ }
+
+ &::-ms-fill {
+ border: none;
+ background-color: currentColor;
+ transition: all 0.2s;
+ }
+
+ &:indeterminate {
+ background-size: 200% 100%;
+ background-image: linear-gradient(
+ to right,
+ transparent 50%,
+ currentColor 50%,
+ currentColor 60%,
+ transparent 60%,
+ transparent 71.5%,
+ currentColor 71.5%,
+ currentColor 84%,
+ transparent 84%
+ );
+ animation: progress-linear 3s infinite linear;
+ }
+
+ &:indeterminate::-moz-progress-bar {
+ background-color: transparent;
+ }
+
+ &:indeterminate::-ms-fill {
+ animation-name: none;
+ }
+
+ @keyframes progress-linear {
+ 0% {
+ background-size: 200% 100%;
+ background-position: left -31.25% top 0%;
+ }
+ 50% {
+ background-size: 800% 100%;
+ background-position: left -49% top 0%;
+ }
+ 100% {
+ background-size: 400% 100%;
+ background-position: left -102% top 0%;
+ }
+ }
+ }
}
diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
index b0a1628d5e..323b344cff 100644
--- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
+++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
@@ -4,7 +4,7 @@
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
-import { ActionValue, DynamicValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix";
+import { ActionValue, DynamicValue, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix";
export type TitleTypeEnum = "attribute" | "expression";
@@ -79,7 +79,6 @@ export interface CalendarContainerProps {
startAttribute?: ListAttributeValue;
endAttribute?: ListAttributeValue;
eventColor?: ListAttributeValue;
- startDateAttribute?: ListAttributeValue;
editable: DynamicValue;
view: ViewEnum;
defaultViewStandard: DefaultViewStandardEnum;
@@ -92,6 +91,7 @@ export interface CalendarContainerProps {
showAllEvents: boolean;
step: number;
timeslots: number;
+ startDateAttribute?: EditableValue;
toolbarItems: ToolbarItemsType[];
customViewShowMonday: boolean;
customViewShowTuesday: boolean;
@@ -134,7 +134,6 @@ export interface CalendarPreviewProps {
startAttribute: string;
endAttribute: string;
eventColor: string;
- startDateAttribute: string;
editable: string;
view: ViewEnum;
defaultViewStandard: DefaultViewStandardEnum;
@@ -147,6 +146,7 @@ export interface CalendarPreviewProps {
showAllEvents: boolean;
step: number | null;
timeslots: number | null;
+ startDateAttribute: string;
toolbarItems: ToolbarItemsPreviewType[];
customViewShowMonday: boolean;
customViewShowTuesday: boolean;