Skip to content

Commit 822beb1

Browse files
HeikoOsigusiobuhov
authored andcommitted
feat: add showMultiDayTimes property to calendar component
1 parent a3f2f1e commit 822beb1

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: [],
@@ -256,3 +263,179 @@ describe("CalendarPropsBuilder validation", () => {
256263
expect(result.timeslots).toBe(2);
257264
});
258265
});
266+
267+
describe("CalendarPropsBuilder showMultiDayTimes", () => {
268+
const mockLocalizer = {
269+
format: jest.fn(),
270+
parse: jest.fn(),
271+
startOfWeek: jest.fn(),
272+
getDay: jest.fn(),
273+
messages: {}
274+
} as any;
275+
276+
it("passes showMultiDayTimes=true to calendar props", () => {
277+
const props = { ...customViewProps, showMultiDayTimes: true };
278+
const builder = new CalendarPropsBuilder(props);
279+
const result = builder.build(mockLocalizer, "en");
280+
expect(result.showMultiDayTimes).toBe(true);
281+
});
282+
283+
it("passes showMultiDayTimes=false to calendar props", () => {
284+
const props = { ...customViewProps, showMultiDayTimes: false };
285+
const builder = new CalendarPropsBuilder(props);
286+
const result = builder.build(mockLocalizer, "en");
287+
expect(result.showMultiDayTimes).toBe(false);
288+
});
289+
});
290+
291+
describe("CalendarPropsBuilder multi-day time formats", () => {
292+
const mockLocalizer = {
293+
format: jest.fn((date: Date, pattern: string, _culture: string) => {
294+
// Simulate locale-aware formatting using the pattern
295+
const hours = date.getHours();
296+
const minutes = date.getMinutes().toString().padStart(2, "0");
297+
return `${hours}:${minutes} (${pattern})`;
298+
}),
299+
parse: jest.fn(),
300+
startOfWeek: jest.fn(),
301+
getDay: jest.fn(),
302+
messages: {}
303+
} as any;
304+
305+
const buildWithTimeFormat = (timeFormatValue: string) => {
306+
const props = {
307+
...customViewProps,
308+
timeFormat: dynamic(timeFormatValue)
309+
};
310+
const builder = new CalendarPropsBuilder(props);
311+
return builder.build(mockLocalizer, "en");
312+
};
313+
314+
it("sets eventTimeRangeStartFormat using the configured time pattern", () => {
315+
const result = buildWithTimeFormat("HH:mm");
316+
const start = new Date("2025-04-28T22:00:00Z");
317+
const end = new Date("2025-04-29T02:00:00Z");
318+
319+
expect(result.formats!.eventTimeRangeStartFormat).toBeDefined();
320+
const label = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
321+
expect(label).toContain("HH:mm");
322+
expect(label).toMatch(/ $/);
323+
});
324+
325+
it("sets eventTimeRangeEndFormat using the configured time pattern", () => {
326+
const result = buildWithTimeFormat("HH:mm");
327+
const start = new Date("2025-04-28T22:00:00Z");
328+
const end = new Date("2025-04-29T02:00:00Z");
329+
330+
expect(result.formats!.eventTimeRangeEndFormat).toBeDefined();
331+
const label = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
332+
expect(label).toContain("HH:mm");
333+
expect(label).toMatch(/^ /);
334+
});
335+
336+
it("uses the same pattern for eventTimeRangeFormat, start, and end formats", () => {
337+
const result = buildWithTimeFormat("h:mm a");
338+
const start = new Date("2025-04-28T22:00:00Z");
339+
const end = new Date("2025-04-29T02:00:00Z");
340+
341+
const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
342+
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
343+
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
344+
345+
// All three should use the same "h:mm a" pattern passed to localizer.format
346+
expect(rangeLabel).toContain("h:mm a");
347+
expect(startLabel).toContain("h:mm a");
348+
expect(endLabel).toContain("h:mm a");
349+
});
350+
351+
it("does not set start/end formats when no timeFormat is configured", () => {
352+
const props = { ...customViewProps, timeFormat: undefined };
353+
const builder = new CalendarPropsBuilder(props);
354+
const result = builder.build(mockLocalizer, "en");
355+
356+
expect(result.formats!.eventTimeRangeStartFormat).toBeUndefined();
357+
expect(result.formats!.eventTimeRangeEndFormat).toBeUndefined();
358+
});
359+
});
360+
361+
describe("CalendarPropsBuilder showEventDate hides multi-day formats", () => {
362+
const mockLocalizer = {
363+
format: jest.fn((_date: Date, pattern: string) => `formatted(${pattern})`),
364+
parse: jest.fn(),
365+
startOfWeek: jest.fn(),
366+
getDay: jest.fn(),
367+
messages: {}
368+
} as any;
369+
370+
it("blanks eventTimeRangeStartFormat when showEventDate=false", () => {
371+
const props = {
372+
...customViewProps,
373+
showEventDate: dynamic(false),
374+
timeFormat: dynamic("HH:mm")
375+
};
376+
const builder = new CalendarPropsBuilder(props);
377+
const result = builder.build(mockLocalizer, "en");
378+
379+
const label = (result.formats!.eventTimeRangeStartFormat as Function)(
380+
{ start: new Date(), end: new Date() },
381+
"en",
382+
mockLocalizer
383+
);
384+
expect(label).toBe("");
385+
});
386+
387+
it("blanks eventTimeRangeEndFormat when showEventDate=false", () => {
388+
const props = {
389+
...customViewProps,
390+
showEventDate: dynamic(false),
391+
timeFormat: dynamic("HH:mm")
392+
};
393+
const builder = new CalendarPropsBuilder(props);
394+
const result = builder.build(mockLocalizer, "en");
395+
396+
const label = (result.formats!.eventTimeRangeEndFormat as Function)(
397+
{ start: new Date(), end: new Date() },
398+
"en",
399+
mockLocalizer
400+
);
401+
expect(label).toBe("");
402+
});
403+
404+
it("blanks eventTimeRangeFormat when showEventDate=false", () => {
405+
const props = {
406+
...customViewProps,
407+
showEventDate: dynamic(false),
408+
timeFormat: dynamic("HH:mm")
409+
};
410+
const builder = new CalendarPropsBuilder(props);
411+
const result = builder.build(mockLocalizer, "en");
412+
413+
const label = (result.formats!.eventTimeRangeFormat as Function)(
414+
{ start: new Date(), end: new Date() },
415+
"en",
416+
mockLocalizer
417+
);
418+
expect(label).toBe("");
419+
});
420+
421+
it("preserves all time range formats when showEventDate=true", () => {
422+
const props = {
423+
...customViewProps,
424+
showEventDate: dynamic(true),
425+
timeFormat: dynamic("p")
426+
};
427+
const builder = new CalendarPropsBuilder(props);
428+
const result = builder.build(mockLocalizer, "en");
429+
430+
const start = new Date("2025-04-28T22:00:00Z");
431+
const end = new Date("2025-04-29T02:00:00Z");
432+
433+
const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
434+
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
435+
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
436+
437+
expect(rangeLabel).not.toBe("");
438+
expect(startLabel).not.toBe("");
439+
expect(endLabel).not.toBe("");
440+
});
441+
});

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
@@ -86,6 +86,7 @@ export class CalendarPropsBuilder {
8686
startAccessor: (event: CalendarEvent) => event.start,
8787
titleAccessor: (event: CalendarEvent) => event.title,
8888
showAllEvents: this.props.showAllEvents,
89+
showMultiDayTimes: this.props.showMultiDayTimes,
8990
min: this.minTime,
9091
max: this.maxTime,
9192
step: this.step,
@@ -165,6 +166,16 @@ export class CalendarPropsBuilder {
165166
culture: string,
166167
loc: DateLocalizer
167168
) => `${formatWith(start, culture, loc)}${formatWith(end, culture, loc)}`;
169+
formats.eventTimeRangeStartFormat = (
170+
{ start }: { start: Date; end: Date },
171+
culture: string,
172+
loc: DateLocalizer
173+
) => `${formatWith(start, culture, loc)} – `;
174+
formats.eventTimeRangeEndFormat = (
175+
{ end }: { start: Date; end: Date },
176+
culture: string,
177+
loc: DateLocalizer
178+
) => ` – ${formatWith(end, culture, loc)}`;
168179
formats.agendaTimeRangeFormat = (
169180
{ start, end }: { start: Date; end: Date },
170181
culture: string,
@@ -264,6 +275,8 @@ export class CalendarPropsBuilder {
264275
// Ensure showEventDate=false always hides event time ranges
265276
if (this.props.showEventDate?.value === false) {
266277
formats.eventTimeRangeFormat = () => "";
278+
formats.eventTimeRangeStartFormat = () => "";
279+
formats.eventTimeRangeEndFormat = () => "";
267280
}
268281

269282
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)