From b79e89468f54c524b7146eeea6436f7ff56afe2f Mon Sep 17 00:00:00 2001 From: nk10nikhil Date: Thu, 9 Apr 2026 18:50:58 +0530 Subject: [PATCH] [scheduler] Add recurrence icon to recurring events in EventTimeline (#22019) --- .../EventTimelinePremium.test.tsx | 29 ++++++++++ .../EventTimelinePremium.tsx | 2 + .../EventTimelinePremiumEvent.tsx | 57 +++++++++++++++++-- .../eventTimelinePremiumClasses.ts | 6 ++ 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx b/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx index f0267955b25f9..fbfca2d13a36c 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx +++ b/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx @@ -128,6 +128,35 @@ describe('', () => { }); }); + it('should display recurrence icon only for recurring events', () => { + const recurringEvent = EventBuilder.new() + .title('Recurring timeline event') + .singleDay('2025-07-03T09:00:00Z') + .resource(engineering) + .recurrent('DAILY') + .build(); + const singleEvent = EventBuilder.new() + .title('Single timeline event') + .singleDay('2025-07-03T11:00:00Z') + .resource(engineering) + .build(); + + renderTimeline({ events: [recurringEvent, singleEvent], view: 'days' }); + + const recurringEventElements = screen.getAllByLabelText(recurringEvent.title); + expect(recurringEventElements.length).to.be.greaterThan(0); + recurringEventElements.forEach((element) => { + expect( + element.querySelector(`.${eventTimelinePremiumClasses.eventRecurringIcon}`), + ).not.to.equal(null); + }); + + const singleEventElement = screen.getByLabelText(singleEvent.title); + expect( + singleEventElement.querySelector(`.${eventTimelinePremiumClasses.eventRecurringIcon}`), + ).to.equal(null); + }); + it('should render events correctly in the time view', () => { const totalWidth = 6144; // 96 hours * 64px const hourBoundaries = { start: 9 * 64, end: 10 * 64 }; // 9:00 - 10:00 diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.tsx b/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.tsx index 02f8fb66dff0d..7f356906031f7 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.tsx +++ b/packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.tsx @@ -52,7 +52,9 @@ const useUtilityClasses = (classes: Partial | undef event: ['event'], eventPlaceholder: ['eventPlaceholder'], eventResizeHandler: ['eventResizeHandler'], + eventContent: ['eventContent'], eventLinesClamp: ['eventLinesClamp'], + eventRecurringIcon: ['eventRecurringIcon'], timeHeader: ['timeHeader'], timeHeaderCell: ['timeHeaderCell'], timeHeaderDayLabel: ['timeHeaderDayLabel'], diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx b/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx index 99bd43754af77..3f6a610c4e0f8 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx +++ b/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import { styled } from '@mui/material/styles'; +import SvgIcon from '@mui/material/SvgIcon'; import { useStore } from '@base-ui/utils/store'; import { useId } from '@base-ui/utils/useId'; import { TimelineGrid } from '@mui/x-scheduler-headless-premium/timeline-grid'; @@ -15,6 +16,8 @@ const ARROW_DEPTH = 8; // px - depth of the chevron point const LEFT_ARROW_CLIP = `polygon(${ARROW_DEPTH}px 0, 100% 0, 100% 100%, ${ARROW_DEPTH}px 100%, 0 50%)`; const RIGHT_ARROW_CLIP = `polygon(0 0, calc(100% - ${ARROW_DEPTH}px) 0, 100% 50%, calc(100% - ${ARROW_DEPTH}px) 100%, 0 100%)`; const BOTH_ARROWS_CLIP = `polygon(${ARROW_DEPTH}px 0, calc(100% - ${ARROW_DEPTH}px) 0, 100% 50%, calc(100% - ${ARROW_DEPTH}px) 100%, ${ARROW_DEPTH}px 100%, 0 50%)`; +const REPEAT_ROUNDED_ICON_PATH = + 'M7 7h10v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V5H6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1s1-.45 1-1zm10 10H7v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.36V19h11c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1s-1 .45-1 1z'; const EventTimelinePremiumEventRoot = styled('div', { name: 'MuiEventTimeline', @@ -85,6 +88,8 @@ const EventTimelinePremiumEventLinesClamp = styled('span', { name: 'MuiEventTimeline', slot: 'EventLinesClamp', })({ + flexGrow: 1, + minWidth: 0, display: '-webkit-box', WebkitLineClamp: 'var(--number-of-lines)', WebkitBoxOrient: 'vertical', @@ -94,6 +99,23 @@ const EventTimelinePremiumEventLinesClamp = styled('span', { overflowWrap: 'break-word', }); +const EventTimelinePremiumEventContent = styled('div', { + name: 'MuiEventTimeline', + slot: 'EventContent', +})(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + minWidth: 0, +})); + +const EventTimelinePremiumEventRecurringIcon = styled(SvgIcon, { + name: 'MuiEventTimeline', + slot: 'EventRecurringIcon', +})({ + flexShrink: 0, +}); + const EventTimelinePremiumEventResizeHandler = styled(TimelineGrid.EventResizeHandler, { name: 'MuiEventTimeline', slot: 'EventResizeHandler', @@ -132,6 +154,7 @@ export const EventTimelinePremiumEvent = React.forwardRef(function EventTimeline ); const isEndResizable = useStore(store, schedulerEventSelectors.isResizable, occurrence.id, 'end'); const color = useStore(store, schedulerEventSelectors.color, occurrence.id); + const isRecurring = useStore(store, schedulerEventSelectors.isRecurring, occurrence.id); // Feature hooks const id = useId(idProp); @@ -159,9 +182,20 @@ export const EventTimelinePremiumEvent = React.forwardRef(function EventTimeline {...sharedProps} className={clsx(sharedProps.className, classes.eventPlaceholder)} > - - {occurrence.title} - + + + {occurrence.title} + + {isRecurring && ( + + )} + ); } @@ -182,9 +216,20 @@ export const EventTimelinePremiumEvent = React.forwardRef(function EventTimeline className={classes.eventResizeHandler} /> )} - - {occurrence.title} - + + + {occurrence.title} + + {isRecurring && ( + + )} + {isEndResizable && ( )} diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/eventTimelinePremiumClasses.ts b/packages/x-scheduler-premium/src/event-timeline-premium/eventTimelinePremiumClasses.ts index fa873ed5a5fbf..5960b76684b31 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/eventTimelinePremiumClasses.ts +++ b/packages/x-scheduler-premium/src/event-timeline-premium/eventTimelinePremiumClasses.ts @@ -41,8 +41,12 @@ export interface EventTimelinePremiumClasses extends EventDialogClasses { eventPlaceholder: string; /** Styles applied to event resize handler elements. */ eventResizeHandler: string; + /** Styles applied to event content elements. */ + eventContent: string; /** Styles applied to event lines clamp elements. */ eventLinesClamp: string; + /** Styles applied to event recurring icon elements. */ + eventRecurringIcon: string; /** Styles applied to the time header root element. */ timeHeader: string; /** Styles applied to time header cell elements. */ @@ -119,7 +123,9 @@ export const eventTimelinePremiumClasses: EventTimelinePremiumClasses = generate 'event', 'eventPlaceholder', 'eventResizeHandler', + 'eventContent', 'eventLinesClamp', + 'eventRecurringIcon', 'timeHeader', 'timeHeaderCell', 'timeHeaderDayLabel',