fix(plugin-calendar): calendar-view consumes or declares every forwarded prop — authored onEventClick can no longer crash a click (#4453) - #4494
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
ACCEPT — step-7 复核 by PM session
Flipping ready + arming auto-merge. #4492 dispatches next with this PR's pattern; #4493/#4454 pool as an enforce-or-remove pair for triage. Generated by Claude Code Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 12, 2026 21:15
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4453.
The defect
calendar-view's renderer passedonEventClick={handleEventClick}and then spread the remaining props after it. Anything arriving under that key won, and both authoring channels reach it — the node's own key and aprops: { … }container:onEventClick: 'NOT-A-FUNCTION'(node key)window error: onEventClick is not a functionprops: { onEventClick: 'NOT-A-FUNCTION' }Worse than an error boundary, which is why it was not cosmetic: React does not route event-handler errors to
SchemaErrorBoundary, so this surfaced as an uncaught window error. The calendar kept looking fine while its click handling was dead.The contract
The #4425 phase-2 ruling (option 1 — promote the whitelist to the SDUI widget contract, comment 5270759246) applied to this widget, per the ruling on this card (comment 5272248819). The raw
{...props}spread intoCalendarViewis gone. The forward set is now exactlyCalendarViewProps, each key resolved to the type that prop declares; everything else is dropped. The rest object is READ for declared keys and never spread — that is the whole of the fix.onEventClickis a declared, function-typed host escape hatch, not a strip and not a lenient coercion: one key, two producers (an SDUI author, whose value can never be a function; a React host, whose passthrough works today and must keep working), and only the value's TYPE can separate them. The discrimination lives in a declared contract at the renderer boundary — the #4435 declared-passthrough pattern — so off-type input gets the one answer every other resolver in this file gives: dropped, the same answer as an absent key.The whole callback family is listed rather than
onEventClickalone, because the defect is the family's: an authoredonDateClick/onNavigate/onViewChange/onEventDrop/onTimeRangeSelect/onAddClickstring killed its own gesture identically, and each is a propCalendarViewalready declares — so this narrows what may reach the component and widens nothing. Four of them (onEventClick,onDateClick,onViewChange,onNavigate) are additionally published ascalendar-view's API incontent/docs/plugins/plugin-calendar.mdx; dropping those would have removed a documented, working host path, which #4433's ruling refused to do silently.This completes the calendar renderer's migration: #4433 (
eventsstrip, PR #4455) and #4452 (currentDateparse, PR #4484) were the first two keys; this card does the rest of the set.Census — every key the old spread could carry
Injected or forwarded by
SchemaRenderer(packages/react/src/SchemaRenderer.tsx, thecreateElementblock):schemaschema.dataand the field-name inputs)className(merged node className + scope class)SchemaRenderersets it AFTER the node'spropscontainer, which is why an authoredprops.classNamewas never an exposure here (verified, pinned)eventspropscontainer (forwarded as a prop of its own),properties,bindariaLabel,ariaDescribedBy,roleand the resolvedaria-label/aria-describedby/roleCalendarViewnames its own region (role="region",aria-label="Calendar"); unchanged from today, where the component ignored themdisabled(always passed as__disabledorundefined)data-obj-id,data-obj-type,data-obj-schema-invalid,data-debug-type,data-debug-idid,name,label,description,placeholder, … (BaseSchema keys)Declared registry inputs (all 11 audited):
dataschema.datatitleField,startDateField,endDateField,allDayField,colorFieldschema.*in the events memoviewcurrentDateDate(#4452), forwardedclassNamecolorMappingallowCreatehandleAddClickit would drive is built and never passed. Dropped here; that is #4454's card and is deliberately NOT fixed in this PRDeclared host escape hatches (typed; nothing here is authorable surface, so the registry
inputslist is untouched):onEventClick,onDateClick,onViewChange,onNavigate,onAddClick,onEventDrop,onTimeRangeSelectlocaleIntl.getCanonicalLocalesacceptsslotMinutesonAction(the renderer's own action channel, consumed not forwarded)Three extras that fell out of the census, all fixed here because they are the same defect at the same boundary:
onActionreached this renderer through the identical props channel and died on the identical click: an authoredonAction: 'NOT-A-FUNCTION'threwonAction is not a function. Typing the forwarded handler and not the consumed one would have been half a fix.localeis a render-time crash channel: MEASURED,toLocaleDateString('en_US')throwsRangeError: Incorrect locale information provided— the underscore spelling a producer writes by accident — and so do'','123','a'. It is kept (a host may legitimately pass one) but validated, rather than dropped or forwarded raw.view:CalendarViewrenders its body undermonth/week/dayonly, so an off-enum value such asagendaproduced a header with no calendar under it at all. Off-enum now gets the absent-key answer, i.e. the component'smonthdefault.Red-first (verbatim)
packages/plugin-calendar/src/calendar-view-renderer.propsContract.test.tsx, run against the pre-fix renderer (git checkout origin/main -- calendar-view-renderer.tsx, restored after): 6 failed | 9 passed (15) — the 6 new pins red, the 9 must-not-change pins green on BOTH sides.Handler errors do not reach
SchemaErrorBoundary, so the pins capture the windowerrorevent, a synchronous throw out offireEvent, andconsole.error, and assert on the union — the environment does not get to decide the verdict by moving the report between channels.Post-fix: all 15 green.
Must-not-change (green on both sides)
SchemaRenderer's trailing props still fires on click with the event payload;onActionwith{ type: 'event-click', payload };onActiondispatch — the precedence the trailing spread used to produce, now restated explicitly instead of falling out of prop ordering;view: 'week'still reaches the component; a node with noviewstill opens on the month grid;locale(de-DE) still reaches the header;classNamestill lands on the calendar region;eventson acalendar-viewnode crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433 pins (calendar-view-renderer.eventsCollision.test.tsx) and the plugin-calendar: authoringcurrentDateas the ISO string its own registry input documents crashes calendar-view #4452 pins (calendar-view-renderer.currentDate.test.tsx) all stay green.Verification
The DOM-leak sweep is #4432's surface and was not edited — run read-only to confirm the calendar targets stay green.
Emitted
.d.tsmeasured both ways (build on this branch vs. with the renderer reverted toorigin/main):packages/plugin-calendar/dist/index.d.tsis byte-identical, 10 lines, no diff. The published surface does not grow, so this is a patch and not a minor.Out of scope, filed
object-calendar/view:calendarspread the authored node into ObjectCalendar — authoredonDateClick/onNavigatethrow uncaught, authoredlocalekills the render #4492 —object-calendar/view:calendarshare one implementation that spreads the authored node intoObjectCalendarthe same way. MEASURED: authoredonDateClickandonNavigatethrow uncaught on a day-cell click and on Next; authoredlocale: 'en_US'takes the render down to the error boundary. (AuthoredonEventClickdid not reproduce there — the parent handler is skipped when the local navigation is an overlay — recorded as an unproven code path rather than asserted.)calendar-view'scolorMappinginput is declared but inert — nothing reads it, so an authored colour map does nothing #4493 —colorMappingis a declared registry input with no read site at all, the same class as plugin-calendar:calendar-view'sallowCreateinput is declared but inert — the handler it would drive is built and never passed #4454.calendar-view'sallowCreateinput is declared but inert — the handler it would drive is built and never passed #4454 (allowCreateinert) is confirmed by this census and deliberately left to its own card.Follow-up for #4425's completion pass (reported, not implemented)
currentDateis now safe to plant as aplugin-calendar:calendar-viewcanary inwidget-dom-leak-sweep.test.tsx: post-#4484 the authored ISO string is parsed at the boundary instead of crashing the component, and post-this-PR it cannot reach any other prop. The same is true of the handler keys — an authoredonEventClickcanary now renders and clicks cleanly. That file is #4432's surface, so the extension belongs to the completion pass, not here.Generated by Claude Code