Skip to content

Commit 5b68966

Browse files
HeikoOsigusrahmanunver
authored andcommitted
feat: add showMultiDayTimes property to calendar component
1 parent 9ce7279 commit 5b68966

5 files changed

Lines changed: 203 additions & 0 deletions

File tree

packages/pluggableWidgets/calendar-web/src/Calendar.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@
124124
<caption>Show all events</caption>
125125
<description>Auto-adjust calendar height to display all events without "more" links</description>
126126
</property>
127+
<property key="showMultiDayTimes" type="boolean" defaultValue="false">
128+
<caption>Show multi-day times</caption>
129+
<description>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</description>
130+
</property>
127131
<property key="step" type="integer" defaultValue="30">
128132
<caption>Step</caption>
129133
<description>Determines the selectable time increments in week and day views</description>

packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ jest.mock("react-big-calendar", () => {
1919
resizable,
2020
selectable,
2121
showAllEvents,
22+
showMultiDayTimes,
23+
min,
24+
max,
2225
events,
2326
step,
2427
timeslots,
@@ -34,6 +37,9 @@ jest.mock("react-big-calendar", () => {
3437
data-resizable={resizable}
3538
data-selectable={selectable}
3639
data-show-all-events={showAllEvents}
40+
data-show-multi-day-times={showMultiDayTimes}
41+
data-min={min?.toISOString()}
42+
data-max={max?.toISOString()}
3743
data-events-count={events?.length ?? 0}
3844
data-step={step}
3945
data-timeslots={timeslots}
@@ -93,6 +99,7 @@ const customViewProps: CalendarContainerProps = {
9399
customViewShowFriday: true,
94100
customViewShowSaturday: false,
95101
showAllEvents: true,
102+
showMultiDayTimes: true,
96103
step: 60,
97104
timeslots: 2,
98105
toolbarItems: [],
@@ -358,3 +365,179 @@ describe("CalendarPropsBuilder column header formats", () => {
358365
expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "HH:mm", "en");
359366
});
360367
});
368+
369+
describe("CalendarPropsBuilder showMultiDayTimes", () => {
370+
const mockLocalizer = {
371+
format: jest.fn(),
372+
parse: jest.fn(),
373+
startOfWeek: jest.fn(),
374+
getDay: jest.fn(),
375+
messages: {}
376+
} as any;
377+
378+
it("passes showMultiDayTimes=true to calendar props", () => {
379+
const props = { ...customViewProps, showMultiDayTimes: true };
380+
const builder = new CalendarPropsBuilder(props);
381+
const result = builder.build(mockLocalizer, "en");
382+
expect(result.showMultiDayTimes).toBe(true);
383+
});
384+
385+
it("passes showMultiDayTimes=false to calendar props", () => {
386+
const props = { ...customViewProps, showMultiDayTimes: false };
387+
const builder = new CalendarPropsBuilder(props);
388+
const result = builder.build(mockLocalizer, "en");
389+
expect(result.showMultiDayTimes).toBe(false);
390+
});
391+
});
392+
393+
describe("CalendarPropsBuilder multi-day time formats", () => {
394+
const mockLocalizer = {
395+
format: jest.fn((date: Date, pattern: string, _culture: string) => {
396+
// Simulate locale-aware formatting using the pattern
397+
const hours = date.getHours();
398+
const minutes = date.getMinutes().toString().padStart(2, "0");
399+
return `${hours}:${minutes} (${pattern})`;
400+
}),
401+
parse: jest.fn(),
402+
startOfWeek: jest.fn(),
403+
getDay: jest.fn(),
404+
messages: {}
405+
} as any;
406+
407+
const buildWithTimeFormat = (timeFormatValue: string) => {
408+
const props = {
409+
...customViewProps,
410+
timeFormat: dynamic(timeFormatValue)
411+
};
412+
const builder = new CalendarPropsBuilder(props);
413+
return builder.build(mockLocalizer, "en");
414+
};
415+
416+
it("sets eventTimeRangeStartFormat using the configured time pattern", () => {
417+
const result = buildWithTimeFormat("HH:mm");
418+
const start = new Date("2025-04-28T22:00:00Z");
419+
const end = new Date("2025-04-29T02:00:00Z");
420+
421+
expect(result.formats!.eventTimeRangeStartFormat).toBeDefined();
422+
const label = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
423+
expect(label).toContain("HH:mm");
424+
expect(label).toMatch(/ $/);
425+
});
426+
427+
it("sets eventTimeRangeEndFormat using the configured time pattern", () => {
428+
const result = buildWithTimeFormat("HH:mm");
429+
const start = new Date("2025-04-28T22:00:00Z");
430+
const end = new Date("2025-04-29T02:00:00Z");
431+
432+
expect(result.formats!.eventTimeRangeEndFormat).toBeDefined();
433+
const label = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
434+
expect(label).toContain("HH:mm");
435+
expect(label).toMatch(/^ /);
436+
});
437+
438+
it("uses the same pattern for eventTimeRangeFormat, start, and end formats", () => {
439+
const result = buildWithTimeFormat("h:mm a");
440+
const start = new Date("2025-04-28T22:00:00Z");
441+
const end = new Date("2025-04-29T02:00:00Z");
442+
443+
const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
444+
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
445+
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
446+
447+
// All three should use the same "h:mm a" pattern passed to localizer.format
448+
expect(rangeLabel).toContain("h:mm a");
449+
expect(startLabel).toContain("h:mm a");
450+
expect(endLabel).toContain("h:mm a");
451+
});
452+
453+
it("does not set start/end formats when no timeFormat is configured", () => {
454+
const props = { ...customViewProps, timeFormat: undefined };
455+
const builder = new CalendarPropsBuilder(props);
456+
const result = builder.build(mockLocalizer, "en");
457+
458+
expect(result.formats!.eventTimeRangeStartFormat).toBeUndefined();
459+
expect(result.formats!.eventTimeRangeEndFormat).toBeUndefined();
460+
});
461+
});
462+
463+
describe("CalendarPropsBuilder showEventDate hides multi-day formats", () => {
464+
const mockLocalizer = {
465+
format: jest.fn((_date: Date, pattern: string) => `formatted(${pattern})`),
466+
parse: jest.fn(),
467+
startOfWeek: jest.fn(),
468+
getDay: jest.fn(),
469+
messages: {}
470+
} as any;
471+
472+
it("blanks eventTimeRangeStartFormat when showEventDate=false", () => {
473+
const props = {
474+
...customViewProps,
475+
showEventDate: dynamic(false),
476+
timeFormat: dynamic("HH:mm")
477+
};
478+
const builder = new CalendarPropsBuilder(props);
479+
const result = builder.build(mockLocalizer, "en");
480+
481+
const label = (result.formats!.eventTimeRangeStartFormat as Function)(
482+
{ start: new Date(), end: new Date() },
483+
"en",
484+
mockLocalizer
485+
);
486+
expect(label).toBe("");
487+
});
488+
489+
it("blanks eventTimeRangeEndFormat when showEventDate=false", () => {
490+
const props = {
491+
...customViewProps,
492+
showEventDate: dynamic(false),
493+
timeFormat: dynamic("HH:mm")
494+
};
495+
const builder = new CalendarPropsBuilder(props);
496+
const result = builder.build(mockLocalizer, "en");
497+
498+
const label = (result.formats!.eventTimeRangeEndFormat as Function)(
499+
{ start: new Date(), end: new Date() },
500+
"en",
501+
mockLocalizer
502+
);
503+
expect(label).toBe("");
504+
});
505+
506+
it("blanks eventTimeRangeFormat when showEventDate=false", () => {
507+
const props = {
508+
...customViewProps,
509+
showEventDate: dynamic(false),
510+
timeFormat: dynamic("HH:mm")
511+
};
512+
const builder = new CalendarPropsBuilder(props);
513+
const result = builder.build(mockLocalizer, "en");
514+
515+
const label = (result.formats!.eventTimeRangeFormat as Function)(
516+
{ start: new Date(), end: new Date() },
517+
"en",
518+
mockLocalizer
519+
);
520+
expect(label).toBe("");
521+
});
522+
523+
it("preserves all time range formats when showEventDate=true", () => {
524+
const props = {
525+
...customViewProps,
526+
showEventDate: dynamic(true),
527+
timeFormat: dynamic("p")
528+
};
529+
const builder = new CalendarPropsBuilder(props);
530+
const result = builder.build(mockLocalizer, "en");
531+
532+
const start = new Date("2025-04-28T22:00:00Z");
533+
const end = new Date("2025-04-29T02:00:00Z");
534+
535+
const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
536+
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
537+
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
538+
539+
expect(rangeLabel).not.toBe("");
540+
expect(startLabel).not.toBe("");
541+
expect(endLabel).not.toBe("");
542+
});
543+
});

packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exports[`Calendar renders correctly with basic props 1`] = `
1313
data-resizable="true"
1414
data-selectable="true"
1515
data-show-all-events="true"
16+
data-show-multi-day-times="true"
1617
data-step="60"
1718
data-testid="mock-calendar"
1819
data-timeslots="2"

packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class CalendarPropsBuilder {
9292
startAccessor: (event: CalendarEvent) => event.start,
9393
titleAccessor: (event: CalendarEvent) => event.title,
9494
showAllEvents: this.props.showAllEvents,
95+
showMultiDayTimes: this.props.showMultiDayTimes,
9596
min: this.minTime,
9697
max: this.maxTime,
9798
step: this.step,
@@ -174,6 +175,16 @@ export class CalendarPropsBuilder {
174175
culture: string,
175176
loc: DateLocalizer
176177
) => `${formatWith(start, culture, loc)}${formatWith(end, culture, loc)}`;
178+
formats.eventTimeRangeStartFormat = (
179+
{ start }: { start: Date; end: Date },
180+
culture: string,
181+
loc: DateLocalizer
182+
) => `${formatWith(start, culture, loc)} – `;
183+
formats.eventTimeRangeEndFormat = (
184+
{ end }: { start: Date; end: Date },
185+
culture: string,
186+
loc: DateLocalizer
187+
) => ` – ${formatWith(end, culture, loc)}`;
177188
formats.agendaTimeRangeFormat = (
178189
{ start, end }: { start: Date; end: Date },
179190
culture: string,
@@ -306,6 +317,8 @@ export class CalendarPropsBuilder {
306317
// Ensure showEventDate=false always hides event time ranges
307318
if (this.props.showEventDate?.value === false) {
308319
formats.eventTimeRangeFormat = () => "";
320+
formats.eventTimeRangeStartFormat = () => "";
321+
formats.eventTimeRangeEndFormat = () => "";
309322
}
310323

311324
return formats;

packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface CalendarContainerProps {
8989
minHour: number;
9090
maxHour: number;
9191
showAllEvents: boolean;
92+
showMultiDayTimes: boolean;
9293
step: number;
9394
timeslots: number;
9495
startDateAttribute?: EditableValue<Date>;
@@ -144,6 +145,7 @@ export interface CalendarPreviewProps {
144145
minHour: number | null;
145146
maxHour: number | null;
146147
showAllEvents: boolean;
148+
showMultiDayTimes: boolean;
147149
step: number | null;
148150
timeslots: number | null;
149151
startDateAttribute: string;

0 commit comments

Comments
 (0)