From 822beb1ba3f5073e2378fb9e34b46a08736c446e Mon Sep 17 00:00:00 2001 From: Heiko Osigus Date: Tue, 3 Mar 2026 12:42:50 +0100 Subject: [PATCH 1/3] feat: add showMultiDayTimes property to calendar component --- .../calendar-web/src/Calendar.xml | 4 + .../src/__tests__/Calendar.spec.tsx | 183 ++++++++++++++++++ .../__snapshots__/Calendar.spec.tsx.snap | 1 + .../src/helpers/CalendarPropsBuilder.ts | 13 ++ .../calendar-web/typings/CalendarProps.d.ts | 2 + 5 files changed, 203 insertions(+) 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..af2293a34f 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 @@ -13,6 +13,7 @@ exports[`Calendar renders correctly with basic props 1`] = ` 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" 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; From 379946f66c9476df674e0e86308870f076c8428e Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 7 May 2026 12:47:06 +0200 Subject: [PATCH 2/3] chore(calendar-web): add changelog entry for showMultiDayTimes property Co-Authored-By: Claude Sonnet 4.5 --- packages/pluggableWidgets/calendar-web/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From 263dd20418335a0816fd4eeb2d3979319b8f26ef Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 7 May 2026 12:57:47 +0200 Subject: [PATCH 3/3] test(calendar-web): update snapshot for min/max date serialization Co-Authored-By: Claude Sonnet 4.5 --- .../src/__tests__/__snapshots__/Calendar.spec.tsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 af2293a34f..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,6 +10,8 @@ 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" @@ -19,9 +21,7 @@ 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]" />