Skip to content

[scheduler] Let consumers open their own edit UI instead of the built-in event dialog #23329

Closed

Description

@john-renterra

Summary

Clicking an event block always opens the built-in EventDialog. There is no prop to stop that or to
substitute our own UI: EventCalendarPremiumProps and EventTimelinePremiumProps expose no slots,
slotProps, renderEvent, onEventClick or onEventDoubleClick, and apiRef exposes only
setVisibleDate (plus goToNextVisibleDate / goToPreviousVisibleDate on the Timeline).

This is not the same request as #22866. That umbrella makes the General tab composable inside
MUI's dialog, and explicitly lists "full form replacement (Option C)" as out of scope. We do not want
to compose sections into your dialog — we need the dialog not to open, so we can open our own
application drawer, which carries our own domain fields, permissions and save path.

#22547 — Phase 2 — Add slots.eventDialog (replace the edit modal) is exactly this, and is currently
closed / "on hold".

Two ways to unblock it, cheapest first. Either one is enough for us.

Option 1 — emit the occurrence key on the Calendar event block (tiny, no new API)

TimelineGridEvent already emits data-occurrence-key, via
TimelineGridEventDataAttributes.occurrenceKey, for its own keyboard navigation.
CalendarGridTimeEvent emits no equivalent — the rendered block carries only role, tabindex,
data-palette, a React-generated id and aria-labelledby, none of which identifies the event.

Because the dialog's trigger binds onClick directly onto the block, a capture-phase listener can
already suppress the dialog. On the Timeline that is all we need, because the attribute tells us
which event was clicked. On the Calendar the same interception works but we cannot identify the
event, so it is useless.

Emitting the same attribute on the Calendar block would be a one-line parity fix, commits you to no
new public API, and would let both surfaces use ~6 lines of consumer code.

Option 2 — reopen slots.eventDialog (#22547), or add onEventClick

The cleaner long-term answer. onEventClick(occurrence, event) with the ability to prevent the
default dialog would also do it, and is a smaller surface than a full slot.

Examples

We are building a dispatch board on EventTimelinePremium (driver lanes) plus EventCalendarPremium
(week/month over the same data). Our acceptance criterion is that clicking a block opens our
scheduling drawer — the same one used elsewhere in the product, with our fields, our permission
gating and our persistence.

What we had to do to get there today:

  • Timeline: capture-phase onClick interception keyed on data-occurrence-key. ~6 lines, works,
    but depends on an attribute you document as internal to keyboard navigation — if it is renamed our
    drawer silently stops opening.
  • Calendar (week/month): no such attribute, so instead we rebuild what EventCalendarPremium
    does — EventCalendarProvider + the three styled contexts + EventDialogProvider +
    EventCalendarRoot — purely so we can place one child inside EventDialogProvider that reads
    useEventDialogContext() and closes the dialog in a useLayoutEffect before it paints.
    EventCalendarPremium mounts the provider itself and overrides children with the licence
    watermark, so there is no other way in.

That second workaround costs three extra direct dependencies, because @mui/x-scheduler/internals
does not resolve from an app that depends only on @mui/x-scheduler-premium:

"@mui/x-scheduler": "9.0.0-beta.7",
"@mui/x-scheduler-internals": "9.0.0-beta.7",
"@mui/x-scheduler-internals-premium": "9.0.0-beta.7",

Related packaging problem

Those four packages are not published in lockstep. As of today @mui/x-scheduler and
@mui/x-scheduler-premium are at 9.0.0-beta.9, while @mui/x-scheduler-internals-premium is only
at 9.0.0-beta.8. Anyone following the documented subpath exports cannot pin a single consistent
version across the set. Worth fixing regardless of the above.

Motivation

Replacing the edit UI is a baseline requirement for embedding a scheduler in an existing product. Our
drawer is not a styled variant of yours — it carries domain fields, role-based gating and a save path
that has to match the rest of the application.

Today the only routes are (a) a DOM-attribute hack that fails silently, available on one component
only, or (b) re-implementing a shipped component so we can inject a single child. Both are things we
would rather not own, and both are fragile across your beta releases.

A single attribute (Option 1) unblocks us with no API commitment on your side. We are a Premium
licence holder and happy to test a canary.

Search keywords: scheduler, event dialog, slots.eventDialog, onEventClick, custom edit UI, data-occurrence-key, EventTimelinePremium, EventCalendarPremium

Order ID: 127623

Activity

  1. added
    support: premium standardSupport request from a Premium standard plan user. https://mui.com/legal/technical-support-sla.
    on Aug 12, 2026
  2. Anexus5919 commented on Aug 14, 2026

    @Anexus5919
    Contributor

    I checked each claim in this issue against current master. Summary of what holds, what needs correcting, and a proposal that fits the existing scheduler conventions.

    Verified

    Claim Result
    No slots / slotProps / renderEvent / onEventClick on the public props Correct
    apiRef exposes only setVisibleDate, plus goToNext/PreviousVisibleDate on the Timeline Correct (SchedulerStore.buildPublicAPI, EventTimelinePremiumStore.buildPublicAPI)
    TimelineGridEvent emits data-occurrence-key, CalendarGridTimeEvent does not Correct (TimelineGridEvent.tsx:180)
    The trigger binds onClick directly on the block, so capture-phase interception works Correct (EventEditingContext.tsx:94-100)
    EventCalendarPremium mounts the provider and overrides children with the watermark Correct (EventCalendarPremium.tsx:81-85)
    The four packages are not published in lockstep Correct, 9.0.0-beta.9 vs 9.0.0-beta.8 on npm today

    Three corrections

    Option 1 is not a one line change. Six call sites open the editing surface, across four components:

    • week/day timed: CalendarGridTimeEvent
    • week/day all-day and month: CalendarGridDayEvent
    • agenda and the "+N more" popover: EventItem, a plain styled div rather than a grid primitive
    • timeline: TimelineGridEvent, the only one emitting the attribute

    Month goes through CalendarGridDayEvent, so patching CalendarGridTimeEvent alone would not cover the week plus month setup described here.

    The Calendar workaround in the report no longer compiles on master. useEventDialogContext was renamed to useEventEditingContext and the provider was restructured around a surface-agnostic EventEditingProvider. The fragility concern is already real rather than hypothetical.

    There is a public escape hatch that avoids the internals packages entirely. SchedulerEvent.className is a documented model field and is applied on all four renderers (TimeGridEvent.tsx:562, DayGridEvent.tsx:417, EventItem.tsx:334, EventTimelinePremiumEvent.tsx:170). Both props types extend React.HTMLAttributes<HTMLDivElement> and the rest props reach the root node, so onClickCapture on the component works today. That removes the three extra dependencies. Two limits: the "+N more" popover is portaled, so a root capture handler misses it, and className identifies the event rather than the individual occurrence.

    Proposal

    onEventClick with cancellation, reusing the pattern the store already uses for onVisibleDateChange, onViewChange, onVisibleResourcesChange and onCollapsedResourcesChange:

    const eventDetails = createChangeEventDetails('none', event?.nativeEvent);
    onVisibleDateChange?.(visibleDate, eventDetails);
    if (!eventDetails.isCanceled && visibleDateProp === undefined) {
      this.set('visibleDate', visibleDate);
    }

    Applied here, on SchedulerParameters:

    /**
     * Event handler called when the user activates an event occurrence (pointer or keyboard).
     * Call `eventDetails.cancel()` to prevent the built-in editing surface from opening.
     */
    onEventClick?: (
      occurrence: SchedulerEventOccurrence,
      eventDetails: SchedulerChangeEventDetails,
    ) => void;

    Fired from EventEditingTrigger, the single funnel every event activation passes through:

    onClick: (event) => {
      onClick?.(event);
      const eventDetails = createChangeEventDetails('none', event.nativeEvent);
      store.parameters.onEventClick?.(occurrence, eventDetails);
      if (eventDetails.isCanceled) {
        return;
      }
      startEditing(ref, occurrence);
    },

    Why this shape:

    • One insertion point covers all six surfaces. Creation drafts call startEditing directly from their own effects, so they correctly do not fire it.
    • SchedulerParameters is the shared base of EventCalendarParameters and EventTimelinePremiumParameters, so the prop reaches the Calendar, the Premium Calendar, the Timeline and the standalone views with no prop drilling.
    • Cancelling suppresses both editing stages, so behaviour matches on mouse and touch. A DOM-level hack that only stops the dialog still leaves the armed toolbar on a coarse pointer since dc1cd3a.
    • Keyboard activation is covered, since useButton synthesizes the click for Enter and Space.
    • No new export, no new package, no renderer changes. Four files, plus pnpm proptypes and pnpm docs:api.

    This does not touch slots.eventDialog (#22547) or the General-tab slot (#22871). It only decides whether the built-in surface opens at all, which is the part neither of those covers.

    I can open a PR for this if the direction looks right.

    Packaging

    The version skew is real, but narrower than it looks. workspace:^ publishes as ^9.0.0-beta.8, which does resolve 9.0.0-beta.9, so normal consumers are unaffected. It only bites when the internals packages are added to an application package.json, which is exactly what the missing API forces today. Probably worth its own issue.

    cc @JCQuintas @LukasTy @rita-codes

  3. self-assigned this
    on Aug 17, 2026
  4. rita-codes commented on Aug 17, 2026

    @rita-codes
    Member

    Thanks for the detailed report, and apologies for the wait @john-renterra . The use case is legitimate, embedding the scheduler in a product that already has its own editing UI is something we want to support properly, and you shouldn't have to own those workarounds.

    We're going with the spirit of your Option 2, with one change: instead of onEventClick, we'll add a cancelable onEventEditingStart callback, available on all scheduler components:

    <EventCalendarPremium
      events={events}
      onEventEditingStart={(occurrence, eventDetails) => {
        eventDetails.cancel();
        openYourDrawer(occurrence.id);
      }}
    />

    The difference: rather than guarding one entry point (the click), it fires whenever the built-in editing UI is about to open, no matter what triggered it — pointer, keyboard, touch, the "+N more" popover, and any entry point we add in the future (context menu, imperative API) — so your drawer can't silently stop working when we ship new features. Calling eventDetails.cancel() prevents our dialog (and the mobile drawer) from opening; if you don't cancel, everything behaves as it does today. It also fires for event creation, so you can intercept a click on an empty slot and open your own creation UI prefilled with the proposed time instead of ours.

    One note on scope: cancelling replaces the built-in dialog — your UI owns the editing form, the recurring-event scope selection when saving from it, and persistence. Everything else keeps the built-in behavior: drag and drop, resize, the recurring scope dialog they trigger, and on touch devices the event toolbar (edit, delete and resize affordances) — there, the callback fires when the user taps the toolbar's Edit action.

    @Anexus5919 thanks a lot for the thorough analysis, it checks out, and the cancellation mechanism you proposed is exactly what we're shipping; we're just moving the insertion point deeper so it covers every entry path. I'll take the implementation from here so we can move fast, since it needs a couple of product decisions I can sync with the team on directly.

    Once this ships you can drop the internals packages entirely, which also takes care of the version skew concern 🙌

    I'll link the PR here shortly!

  5. Anexus5919 commented on Aug 17, 2026

    @Anexus5919
    Contributor

    we're just moving the insertion point deeper so it covers every entry path. I'll take the implementation from here so we can move fast, since it needs a couple of product decisions I can sync with the team on directly.

    Makes sense 👍

    @rita-codes One thing worth checking on the creation case. Both creation entry points only set the placeholder and never clear it (useEventCreation.ts:34 for pointer, useKeyboardEventCreation.ts:32 for keyboard); the only reachable clear is stopEditing() (the other setOccurrencePlaceholder(null) calls are all drag and resize paths). So on a cancel, editing never starts, nothing clears the placeholder, and the creation preview stays on the grid.

    The re-fire guard has the same root. isEditingPlaceholder in TimeGridColumn.tsx:176-182 (same shape in DayGridCell and MonthViewCell) only flips once store.startEditing has run, so a cancel leaves it false and the churn it was added to absorb comes back.

    Smaller one: since it fires for both cases, consumers need to tell create from edit, and (occurrence, eventDetails) doesn't carry that. BaseUIChangeEventDetails is generic over its reason string if you want it explicit.

  6. rita-codes commented on Aug 18, 2026

    @rita-codes
    Member

    Good points @Anexus5919 🙌

    The first two are already handled in my implementation: the cancel branch in startEditing drops the creation placeholder, so no stale preview on the grid and the re-fire guard never comes into play, there's a test pinning both.

    The reason suggestion is a good catch, adopting it! The callback will fire with eventDetails.reason: 'creation' | 'edit' so consumers don't have to infer the case from the occurrence. It'll be in the PR.

  7. rita-codes commented on Aug 18, 2026

    @rita-codes
    Member

    Heyy @john-renterra ! The implementation is up in #23361 and you can already try it against your app:

    pnpm add https://pkg.pr.new/mui/mui-x/@mui/x-scheduler@23361 https://pkg.pr.new/mui/mui-x/@mui/x-scheduler-premium@23361

    Those are preview builds of the PR's latest commit, with all four packages pinned to the same version, no internals packages needed anymore, and the version skew issue can't occur here.

    What you get:

    <EventTimelinePremium
      onEventEditingStart={(occurrence, eventDetails) => {
        eventDetails.cancel();
        openYourDrawer(occurrence.id);
      }}
    />

    The callback fires right before the built-in dialog opens, for every entry point (pointer, keyboard, touch, the "+N more" popover..) on both the Calendar and the Timeline. It also fires for event creation (eventDetails.reason is "creation", with the proposed dates on the occurrence), so you can open your own creation UI prefilled instead of disabling creation altogether. Docs for the released version will live on the Editing pages of both components.

    If everything works on your side, please confirm here, barring surprises this should make this week's release. And if you hit anything, this is the perfect moment to tell us 🙌

  8. john-renterra commented on Aug 20, 2026

    @john-renterra
    Author

    Thank you for the incredibly quick turnaround on this, @rita-codes — going from filed to a working PR in a matter of days is hugely appreciated.

    We pulled #23361 down and validated it against our use case, and onEventEditingStart is exactly what we needed. Being able to cancel() the built-in dialog and open our own edit UI — across pointer, keyboard, and creation, with the edit/view/creation reason and the read-only handling — covers both surfaces we were struggling with. On our side this lets us delete a ~90-line reimplementation of EventCalendarPremium on the Week/Month views and remove a fragile capture-phase click interception on the Timeline that depended on the undocumented data-occurrence-key attribute, along with the extra internal-package dependencies those workarounds pulled in.

    Looking forward to seeing it land. Thanks again to you and @flaviendelangle for the fast, thorough work.

  9. github-actions commented on Aug 21, 2026

    @github-actions
    Contributor

    This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
    Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.


    @john-renterra Thanks again for creating this issue! If you have a moment, we would love to hear your thoughts on how we handled it with this short feedback form.

  10. rita-codes commented on Aug 21, 2026

    @rita-codes
    Member

    Thank you for the incredibly quick turnaround on this, @rita-codes — going from filed to a working PR in a matter of days is hugely appreciated.

    We pulled #23361 down and validated it against our use case, and onEventEditingStart is exactly what we needed. Being able to cancel() the built-in dialog and open our own edit UI — across pointer, keyboard, and creation, with the edit/view/creation reason and the read-only handling — covers both surfaces we were struggling with. On our side this lets us delete a ~90-line reimplementation of EventCalendarPremium on the Week/Month views and remove a fragile capture-phase click interception on the Timeline that depended on the undocumented data-occurrence-key attribute, along with the extra internal-package dependencies those workarounds pulled in.

    Looking forward to seeing it land. Thanks again to you and @flaviendelangle for the fast, thorough work.

    Thanks a lot for the kind words and for taking the time to validate the PR against your use case! Really glad to hear the new API lets you get rid of those workarounds 👏

    The feature has just been merged! We’re planning to publish a new release today, so it should be available as soon as the new version is out.

    Thanks again for the detailed feedback! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

scope: schedulerChanges related to the scheduler.support: premium standardSupport request from a Premium standard plan user. https://mui.com/legal/technical-support-sla.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions