diff --git a/packages/pluggableWidgets/calendar-web/CHANGELOG.md b/packages/pluggableWidgets/calendar-web/CHANGELOG.md
index 3d7b78ae98..705000df86 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]
+### Added
+
+- We added a `showMultiDayTimes` property to control whether start/end times are displayed for multi-day events in the calendar.
+
## [2.4.0] - 2026-03-20
### Fixed
diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml
index 25e9204adf..ed16f9e0f0 100644
--- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml
+++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml
@@ -124,6 +124,10 @@
Show all events
Auto-adjust calendar height to display all events without "more" links
+
+ Show multi-day times
+ Show start and end times for events that span multiple days in the week and day views instead of placing them in the all-day row
+
Step
Determines the selectable time increments in week and day views
diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
index 6174878704..876066f1ed 100644
--- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
+++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx
@@ -19,6 +19,9 @@ jest.mock("react-big-calendar", () => {
resizable,
selectable,
showAllEvents,
+ showMultiDayTimes,
+ min,
+ max,
events,
step,
timeslots,
@@ -34,6 +37,9 @@ jest.mock("react-big-calendar", () => {
data-resizable={resizable}
data-selectable={selectable}
data-show-all-events={showAllEvents}
+ data-show-multi-day-times={showMultiDayTimes}
+ data-min={min?.toISOString()}
+ data-max={max?.toISOString()}
data-events-count={events?.length ?? 0}
data-step={step}
data-timeslots={timeslots}
@@ -93,6 +99,7 @@ const customViewProps: CalendarContainerProps = {
customViewShowFriday: true,
customViewShowSaturday: false,
showAllEvents: true,
+ showMultiDayTimes: true,
step: 60,
timeslots: 2,
toolbarItems: [],
@@ -256,3 +263,179 @@ describe("CalendarPropsBuilder validation", () => {
expect(result.timeslots).toBe(2);
});
});
+
+describe("CalendarPropsBuilder showMultiDayTimes", () => {
+ const mockLocalizer = {
+ format: jest.fn(),
+ parse: jest.fn(),
+ startOfWeek: jest.fn(),
+ getDay: jest.fn(),
+ messages: {}
+ } as any;
+
+ it("passes showMultiDayTimes=true to calendar props", () => {
+ const props = { ...customViewProps, showMultiDayTimes: true };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+ expect(result.showMultiDayTimes).toBe(true);
+ });
+
+ it("passes showMultiDayTimes=false to calendar props", () => {
+ const props = { ...customViewProps, showMultiDayTimes: false };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+ expect(result.showMultiDayTimes).toBe(false);
+ });
+});
+
+describe("CalendarPropsBuilder multi-day time formats", () => {
+ const mockLocalizer = {
+ format: jest.fn((date: Date, pattern: string, _culture: string) => {
+ // Simulate locale-aware formatting using the pattern
+ const hours = date.getHours();
+ const minutes = date.getMinutes().toString().padStart(2, "0");
+ return `${hours}:${minutes} (${pattern})`;
+ }),
+ parse: jest.fn(),
+ startOfWeek: jest.fn(),
+ getDay: jest.fn(),
+ messages: {}
+ } as any;
+
+ const buildWithTimeFormat = (timeFormatValue: string) => {
+ const props = {
+ ...customViewProps,
+ timeFormat: dynamic(timeFormatValue)
+ };
+ const builder = new CalendarPropsBuilder(props);
+ return builder.build(mockLocalizer, "en");
+ };
+
+ it("sets eventTimeRangeStartFormat using the configured time pattern", () => {
+ const result = buildWithTimeFormat("HH:mm");
+ const start = new Date("2025-04-28T22:00:00Z");
+ const end = new Date("2025-04-29T02:00:00Z");
+
+ expect(result.formats!.eventTimeRangeStartFormat).toBeDefined();
+ const label = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
+ expect(label).toContain("HH:mm");
+ expect(label).toMatch(/– $/);
+ });
+
+ it("sets eventTimeRangeEndFormat using the configured time pattern", () => {
+ const result = buildWithTimeFormat("HH:mm");
+ const start = new Date("2025-04-28T22:00:00Z");
+ const end = new Date("2025-04-29T02:00:00Z");
+
+ expect(result.formats!.eventTimeRangeEndFormat).toBeDefined();
+ const label = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
+ expect(label).toContain("HH:mm");
+ expect(label).toMatch(/^ – /);
+ });
+
+ it("uses the same pattern for eventTimeRangeFormat, start, and end formats", () => {
+ const result = buildWithTimeFormat("h:mm a");
+ const start = new Date("2025-04-28T22:00:00Z");
+ const end = new Date("2025-04-29T02:00:00Z");
+
+ const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
+ const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
+ const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
+
+ // All three should use the same "h:mm a" pattern passed to localizer.format
+ expect(rangeLabel).toContain("h:mm a");
+ expect(startLabel).toContain("h:mm a");
+ expect(endLabel).toContain("h:mm a");
+ });
+
+ it("does not set start/end formats when no timeFormat is configured", () => {
+ const props = { ...customViewProps, timeFormat: undefined };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+
+ expect(result.formats!.eventTimeRangeStartFormat).toBeUndefined();
+ expect(result.formats!.eventTimeRangeEndFormat).toBeUndefined();
+ });
+});
+
+describe("CalendarPropsBuilder showEventDate hides multi-day formats", () => {
+ const mockLocalizer = {
+ format: jest.fn((_date: Date, pattern: string) => `formatted(${pattern})`),
+ parse: jest.fn(),
+ startOfWeek: jest.fn(),
+ getDay: jest.fn(),
+ messages: {}
+ } as any;
+
+ it("blanks eventTimeRangeStartFormat when showEventDate=false", () => {
+ const props = {
+ ...customViewProps,
+ showEventDate: dynamic(false),
+ timeFormat: dynamic("HH:mm")
+ };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+
+ const label = (result.formats!.eventTimeRangeStartFormat as Function)(
+ { start: new Date(), end: new Date() },
+ "en",
+ mockLocalizer
+ );
+ expect(label).toBe("");
+ });
+
+ it("blanks eventTimeRangeEndFormat when showEventDate=false", () => {
+ const props = {
+ ...customViewProps,
+ showEventDate: dynamic(false),
+ timeFormat: dynamic("HH:mm")
+ };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+
+ const label = (result.formats!.eventTimeRangeEndFormat as Function)(
+ { start: new Date(), end: new Date() },
+ "en",
+ mockLocalizer
+ );
+ expect(label).toBe("");
+ });
+
+ it("blanks eventTimeRangeFormat when showEventDate=false", () => {
+ const props = {
+ ...customViewProps,
+ showEventDate: dynamic(false),
+ timeFormat: dynamic("HH:mm")
+ };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+
+ const label = (result.formats!.eventTimeRangeFormat as Function)(
+ { start: new Date(), end: new Date() },
+ "en",
+ mockLocalizer
+ );
+ expect(label).toBe("");
+ });
+
+ it("preserves all time range formats when showEventDate=true", () => {
+ const props = {
+ ...customViewProps,
+ showEventDate: dynamic(true),
+ timeFormat: dynamic("p")
+ };
+ const builder = new CalendarPropsBuilder(props);
+ const result = builder.build(mockLocalizer, "en");
+
+ const start = new Date("2025-04-28T22:00:00Z");
+ const end = new Date("2025-04-29T02:00:00Z");
+
+ const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
+ const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
+ const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
+
+ expect(rangeLabel).not.toBe("");
+ expect(startLabel).not.toBe("");
+ expect(endLabel).not.toBe("");
+ });
+});
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 941e99e4ec..6f749c5aa6 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,17 +10,18 @@ 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"
+ data-show-multi-day-times="true"
data-step="60"
data-testid="mock-calendar"
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 d6176e11a6..d6e1d907ab 100644
--- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts
+++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts
@@ -86,6 +86,7 @@ export class CalendarPropsBuilder {
startAccessor: (event: CalendarEvent) => event.start,
titleAccessor: (event: CalendarEvent) => event.title,
showAllEvents: this.props.showAllEvents,
+ showMultiDayTimes: this.props.showMultiDayTimes,
min: this.minTime,
max: this.maxTime,
step: this.step,
@@ -165,6 +166,16 @@ export class CalendarPropsBuilder {
culture: string,
loc: DateLocalizer
) => `${formatWith(start, culture, loc)} – ${formatWith(end, culture, loc)}`;
+ formats.eventTimeRangeStartFormat = (
+ { start }: { start: Date; end: Date },
+ culture: string,
+ loc: DateLocalizer
+ ) => `${formatWith(start, culture, loc)} – `;
+ formats.eventTimeRangeEndFormat = (
+ { end }: { start: Date; end: Date },
+ culture: string,
+ loc: DateLocalizer
+ ) => ` – ${formatWith(end, culture, loc)}`;
formats.agendaTimeRangeFormat = (
{ start, end }: { start: Date; end: Date },
culture: string,
@@ -264,6 +275,8 @@ export class CalendarPropsBuilder {
// Ensure showEventDate=false always hides event time ranges
if (this.props.showEventDate?.value === false) {
formats.eventTimeRangeFormat = () => "";
+ formats.eventTimeRangeStartFormat = () => "";
+ formats.eventTimeRangeEndFormat = () => "";
}
return formats;
diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
index 323b344cff..46a94c190d 100644
--- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
+++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts
@@ -89,6 +89,7 @@ export interface CalendarContainerProps {
minHour: number;
maxHour: number;
showAllEvents: boolean;
+ showMultiDayTimes: boolean;
step: number;
timeslots: number;
startDateAttribute?: EditableValue;
@@ -144,6 +145,7 @@ export interface CalendarPreviewProps {
minHour: number | null;
maxHour: number | null;
showAllEvents: boolean;
+ showMultiDayTimes: boolean;
step: number | null;
timeslots: number | null;
startDateAttribute: string;