Package Exports
- @cision/eslint-plugin-react-custom-element-events
- @cision/eslint-plugin-react-custom-element-events/src/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@cision/eslint-plugin-react-custom-element-events) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
eslint-plugin-react-custom-element-events
- ESLint rules that catch custom element event binding mistakes in React 19+ (auto-fixable).
- A test suite that proves which event prop patterns fire and which don't.
Quick Start
npm install --save-dev @cision/eslint-plugin-react-custom-element-eventseslint.config.js (flat config):
const customElementEvents = require('@cision/eslint-plugin-react-custom-element-events');
module.exports = [
{
plugins: {
'cision-react-custom-element-events': customElementEvents,
},
rules: {
'cision-react-custom-element-events/no-undercased-react-event-on-custom-element': 'error',
'cision-react-custom-element-events/no-suspicious-camelcase-event-on-custom-element': 'warn',
'cision-react-custom-element-events/no-camelcase-nondelegated-event-on-custom-element': 'error',
},
},
];| Rule | Severity | Fix | What it catches |
|---|---|---|---|
cision-react-custom-element-events/no-undercased-react-event-on-custom-element |
error |
autofix | ondrag → onDrag (known React event in wrong case) |
cision-react-custom-element-events/no-suspicious-camelcase-event-on-custom-element |
warn |
suggestion | onMyCamelCasedEvent → onmyCamelCasedEvent (likely wrong casing) |
cision-react-custom-element-events/no-camelcase-nondelegated-event-on-custom-element |
error |
autofix | onClose → onclose (non-delegated event that React never wires on custom elements) |
Versioning & event name list
The rules read from two generated data files:
src/data/reactEventNames.json— 180 mappings of all known React synthetic events (e.g."ondrag"→"onDrag")src/data/nonDelegatedEventNames.json— 32 mappings of non-delegated events that must stay lowercase on custom elements (e.g."onclose"→"onClose")
Both are generated by parsing the installed react-dom development bundle (npm run generate).
Automatic publishing: A scheduled GitHub Action checks for new React releases daily. When the event registry changes it regenerates the data files, runs tests, bumps to a new minor version, and publishes automatically.
Manual publishing: Rule logic fixes are released manually. Push a version tag (e.g. v1.2.0) and the publish workflow runs tests and publishes to npm.
Requires react-dom >=19.0.0 (declared in peerDependencies).
How event props work on custom elements
React 19 handles on* props on custom elements differently depending on the prop name. Getting the casing wrong causes silent failures — your handler never fires and there's no error.
camelCase works for standard React events
Props like onDrag, onClick, onChange, onFocus, onKeyDown, etc. work as expected. React routes them through its synthetic event system and your handler receives a SyntheticEvent.
<my-el onDrag={handler} /> // ✓ works
<my-el onClick={handler} /> // ✓ works
<my-el onChange={handler} /> // ✓ worksWatch out for lowercase: ondrag, onclick, onchange look plausible but misses React's registry (the lookup is case-sensitive). The handler still fires, but outside React's event system — stopPropagation() behaves differently, no SyntheticEvent, no scheduler priority.
For some events, only lowercase works
Events like close, cancel, toggle, load, error, and all media events (play, pause, ended, etc.) require lowercase props on custom elements. Despite being known React events, the camelCase form (onClose, onCancel, etc.) silently never fires.
This is because React wires these directly on specific native elements like <dialog> and <video> — but never for custom elements. The lowercase form works because it bypasses React's registry and lands on a direct addEventListener('close', fn) on the element.
<my-el onClose={handler} /> // ✗ never fires
<my-el onclose={handler} /> // ✓ works
<my-el onCancel={handler} /> // ✗ never fires
<my-el oncancel={handler} /> // ✓ works
<my-el onPlay={handler} /> // ✗ never fires
<my-el onplay={handler} /> // ✓ worksFull list of affected events:
close,cancel,toggle,beforetoggle,load,error,scroll,scrollend,invalid, and all HTMLMediaElement events (play,pause,ended,timeupdate,volumechange, etc.).
For custom events, use on + the exact event name
Prop names React doesn't recognize get passed verbatim to addEventListener — the on prefix is stripped and the rest is used as-is, including case.
<my-el oncustomevent={handler} /> // ✓ listens for 'customevent'
<my-el onslotchange={handler} /> // ✓ listens for 'slotchange'
<my-el onmyWidgetToggle={handler} /> // ✓ listens for 'myWidgetToggle'
<my-el onMyWidgetToggle={handler} /> // ✗ listens for 'MyWidgetToggle' — probably wrongIf your element dispatches new CustomEvent('myWidgetToggle'), the correct prop is onmyWidgetToggle (lowercase m). Writing onMyWidgetToggle listens for MyWidgetToggle (capital M) and silently misses the event.
Test Suite
src/__tests__/custom-element-events.test.jsx has passing behavioral tests proving each of the above cases.
npm install
npm testESLint Rules
no-undercased-react-event-on-custom-element
Flags lowercase on* props on custom elements that match a known React delegated synthetic event (e.g. ondrag, onclick, onchange). Auto-fixable via eslint --fix.
Non-delegated events (onclose, oncancel, ontoggle, etc.) are intentionally excluded — see no-camelcase-nondelegated-event-on-custom-element below.
// ✗ ERROR (auto-fixed) — ondrag → onDrag
<my-el ondrag={handler} />
<my-el onclick={handler} />
<my-el onchange={handler} />
<my-el ondoubleclick={handler} />no-suspicious-camelcase-event-on-custom-element
Warns when an unknown camelCased on* prop is used on a custom element. onMyCamelCasedEvent would listen for MyCamelCasedEvent (capital M), which is almost certainly not the intended event name. Provides a suggestion (not an autofix) to lowercase the first letter.
// ⚠ WARNING (suggestion) — onMyCamelCasedEvent → onmyCamelCasedEvent
<my-el onMyCamelCasedEvent={handler} />
<my-el onWidgetToggle={handler} />no-camelcase-nondelegated-event-on-custom-element
Flags camelCase non-delegated event props on custom elements. React never calls listenToNonDelegatedEvent() for custom elements, so onClose silently never fires. The lowercase form (onclose) is the correct usage — it routes through path 3 (direct addEventListener). Auto-fixable via eslint --fix.
Affected events: close, cancel, toggle, beforetoggle, load, error, scroll, scrollend, invalid, and all media events (play, pause, ended, timeupdate, etc.).
// ✗ ERROR (auto-fixed) — onClose → onclose
<my-el onClose={handler} /> // never fires — React has no non-delegated listener for custom elements
<my-el onCancel={handler} />
<my-el onToggle={handler} />
<my-el onLoad={handler} />
<my-el onError={handler} />
<my-el onPlay={handler} />
<my-el onEnded={handler} />
// ✓ Correct — lowercase routes through path 3 (direct addEventListener)
<my-el onclose={handler} />
<my-el oncancel={handler} />
<my-el onplay={handler} />What no rule flags
// ✓ Correct camelCase — known React delegated event, uses synthetic event system
<my-el onDrag={handler} />
<my-el onClick={handler} />
<my-el onChange={handler} />
// ✓ Non-delegated events in correct lowercase form
<my-el onclose={handler} />
<my-el oncancel={handler} />
<my-el onplay={handler} />
// ✓ Truly custom event — intentional direct listener
<my-el oncustomevent={handler} />
<my-el onslotchange={handler} />
<my-el onmyCamelCasedEvent={handler} />
// ✓ Not a custom element — out of scope
<div ondrag={handler} />
// ✓ SVG/MathML elements excluded by React's own isCustomElement() check
<annotation-xml ondrag={handler} />
<color-profile onchange={handler} />Project structure
scripts/
generate-event-names.mjs — parses installed react-dom → writes both data files
src/
data/
reactEventNames.json — generated; 180 mappings (including Capture variants)
nonDelegatedEventNames.json — generated; 32 mappings of non-delegated events
__tests__/
custom-element-events.test.jsx — behavioral tests (paths 1–3 + non-delegated)
lint-rule.test.js — RuleTester: no-undercased-react-event-on-custom-element
lint-rule-camelcase.test.js — RuleTester: no-suspicious-camelcase-event-on-custom-element
lint-rule-nondelegated.test.js — RuleTester: no-camelcase-nondelegated-event-on-custom-element
lint-rule-integration.test.js — cross-rule conflict tests
rules/
no-undercased-react-event-on-custom-element.js
no-suspicious-camelcase-event-on-custom-element.js
no-camelcase-nondelegated-event-on-custom-element.js
index.js — ESLint plugin export (all three rules)
.github/workflows/
check-react-update.yml — scheduled: auto-publish on React event registry changes
publish.yml — tag-triggered: publish on v* tags