Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

plugin-calendar: an authored onEventClick reaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453

Closed

Description

@yinlianghui

Measured while grading #4433's collision surface (that card's condition 1b: sweep the renderer's other computed props for the same authored-key overwrite exposure). events was crash-capable and is fixed. This one is the remaining exposure, and it is filed rather than fixed because closing it needs a decision the #4433 ruling did not cover.

The exposure

calendar-view's renderer passes onEventClick={handleEventClick} and then spreads {...props} after it. Anything arriving under that key wins — the same shape #4433 fixed for events.

Measured, both channels, before and after #4433's fix (unchanged by it):

authored renders? click
onEventClick: 'NOT-A-FUNCTION' (node key) yes, normally window error: onEventClick is not a function
props: { onEventClick: 'NOT-A-FUNCTION' } yes, normally same

Note the failure mode is worse than an error boundary, which is why it is not simply cosmetic: React does not route event-handler errors to SchemaErrorBoundary, so this surfaces as an uncaught window error. The calendar keeps looking fine and its click handling is dead.

Why this was not fixed alongside events

The two keys are not the same case, and the difference is what needs deciding.

events is injected metadata: SchemaRenderer forwards a node's events key as a prop, no code in the renderer layer consumes it, and no host has a reason to pass one to this renderer. Dropping it is free.

onEventClick arrives through the same channel for two very different producers:

  1. an SDUI author writing JSON — the value can never be a function, so it is always broken; and
  2. a React host rendering SchemaRenderer schema={…} onEventClick={fn} — the trailing ...props spread makes this work today, and it is the component's genuine escape hatch (the same passthrough MetricWidget deliberately keeps).

The key name cannot separate them; only the value's type can. So the options are:

C and D are the same direction at two altitudes and are probably the real answer; A and B are local patches. This is left for triage rather than guessed at.

Same grading round, for the record: className is not exposed — the renderer destructures it out of its own props, so the rest object cannot carry it, and SchemaRenderer sets className after the node's props container anyway (measured: an authored props.className never reaches the region).

Related: #4433 (the events collision, fixed), #4425 (the phase-2 whitelist question), #4357 / #4428 (the deny-list precedent).


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    ContributorAuthor

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4453-calendar-props-contract.

    Ruling (delegated authority; open veto window): option D, now concrete — the #4425 phase-2 whitelist contract is ruled (comment 5270759246 there) and its calendar migration resolves this card's A–D question:

    1. Convert calendar-view's renderer to consume-or-declare: stop the raw {...props} spread into CalendarView. Declared registry inputs are consumed (the plugin-calendar: authoring events on a calendar-view node crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433 events strip and fix(plugin-calendar): authored ISO currentDate is parsed to a Date instead of crashing the calendar (#4452) #4484 currentDate parse already are — this completes the set); injected props consumed per the contract.
    2. onEventClick becomes a DECLARED host escape hatch, function-typed — the fix(plugin-dashboard): declare the DOM pass-through the metric props accept (#4426) #4435 declared-passthrough pattern, which is what makes the hatch legal under the contract: a host-passed FUNCTION is forwarded to CalendarView's prop (the working path the card forbids breaking); a NON-FUNCTION value (the authored-JSON crash case — a string can never be a handler) is dropped = abstain, same answer as absent. This is not option B's lenient-coercion shape because the discrimination now lives in a declared contract at the renderer boundary, not ad-hoc in a consumer — the key is declared, its type is declared, off-type input gets the one answer.
    3. Sweep the renderer's remaining forwarded keys against the same rule; every key is either a declared input (consumed), a declared hatch (typed), or dropped. Name the census in the PR.

    Red-first: the card's own measurements — authored onEventClick: 'NOT-A-FUNCTION' (node key AND props container channel, per #4484's lesson) → pre-fix click throws the uncaught onEventClick is not a function window error; post-fix click is a no-op (abstain). Must-not-change: a host-passed function still fires on click (green both sides); the #4433/#4484 pins stay green.

    ⛔ Sweep/ledger file: widget-dom-leak-sweep.test.tsx is in-flight #4432's surface — do NOT touch it. The now-plantable currentDate canary (post-#4484) and any calendar canary-set extension are REPORTED as a follow-up for #4425's completion pass, not edited here.

    Grading: patch @object-ui/plugin-calendar (runtime boundary change; .d.ts-checked — the registry declaration if it moves is measured, minor per precedent if the published surface grows).

    Surface (mutual exclusion): packages/plugin-calendar/** + one changeset. Disjoint from in-flight #4474 (organizations), #4336 (detail lookup), #4408 (list error state), #4432 (dashboard + sweep file). #4454 (inert allowCreate) stays its own card — do not fix it here, but if your census confirms it, note it.


    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions