JSPM

@cision/eslint-plugin-react-custom-element-events

1.1.9
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 131
  • Score
    100M100P100Q108915F
  • License ISC

ESLint plugin and test suite for React 19 custom element event binding

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 reference for how React 19 routes different on* props on custom elements.

Playground

Use the ESLint plugin to validate any prop/element pair, right from the browser

https://cision.github.io/eslint-plugin-react-custom-element-events


Quick Start

npm install --save-dev @cision/eslint-plugin-react-custom-element-events

eslint.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-camelcase-nondelegated-event-on-custom-element': 'error',
      'cision-react-custom-element-events/no-suspicious-camelcase-event-on-custom-element': 'warn',
    },
  },
];
Rule Severity Fix What it catches
cision-react-custom-element-events/no-undercased-react-event-on-custom-element error autofix ondragonDrag (known React event in wrong case)
cision-react-custom-element-events/no-camelcase-nondelegated-event-on-custom-element error autofix onCloseonclose (non-delegated event that React never wires on custom elements)
cision-react-custom-element-events/no-suspicious-camelcase-event-on-custom-element warn suggestion onMyCamelCasedEventonmyCamelCasedEvent (likely wrong casing)

Versioning & event name list

The rules read from two generated data files:

Both are generated by parsing the installed react-dom development bundle (npm run generate).

Semver policy: Patch releases are for non-functional changes (README, comments, refactors). Minor releases are for functional changes (new/updated rules, event name data updates). Major releases are for breaking API changes.

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 and non-functional updates 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

Test suite proof: must-be-camelcase.test.js · react-integration.test.jsx

The following events work with camelCase props — React routes them through its synthetic event system and your handler receives a SyntheticEvent.

Category Props
Mouse onClick onDoubleClick onAuxClick onContextMenu onMouseDown onMouseUp onMouseMove onMouseEnter onMouseLeave onMouseOver onMouseOut
Keyboard onKeyDown onKeyUp onKeyPress
Focus onFocus onBlur
Form / input onChange onInput onBeforeInput onSelect onSubmit onReset
Drag onDrag onDragStart onDragEnd onDragEnter onDragLeave onDragOver onDragExit onDrop
Pointer onPointerDown onPointerUp onPointerMove onPointerEnter onPointerLeave onPointerOver onPointerOut onPointerCancel onGotPointerCapture onLostPointerCapture
Touch onTouchStart onTouchEnd onTouchMove onTouchCancel
Wheel onWheel
Clipboard onCopy onCut onPaste
Composition onCompositionStart onCompositionEnd onCompositionUpdate
Animation onAnimationStart onAnimationEnd onAnimationIteration
Transition onTransitionStart onTransitionEnd onTransitionRun onTransitionCancel

All of the above also have a ...Capture variant (e.g. onClickCapture) for the capture phase.

<my-el onDrag={handler} />    // ✓ works
<my-el onClick={handler} />   // ✓ works
<my-el onChange={handler} />  // ✓ works

Watch out for lowercase: ondrag, onclick, onchange look plausible but miss 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

Test suite proof: must-be-lowercase.test.js · react-integration.test.jsx

The following events require lowercase props on custom elements. Despite appearing in React's event registry, the camelCase form 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 on the element.

Category Use these (lowercase) Not these (camelCase — silent no-op)
Dialog onclose oncancel onClose onCancel
Details / popover ontoggle onbeforetoggle onToggle onBeforeToggle
Resource loading onload onerror onabort onLoad onError onAbort
Form oninvalid onInvalid
Scroll onscroll onscrollend onScroll onScrollEnd
Resize onresize onResize
Media oncanplay oncanplaythrough ondurationchange onemptied onencrypted onended onloadeddata onloadedmetadata onloadstart onpause onplay onplaying onprogress onratechange onseeked onseeking onstalled onsuspend ontimeupdate onvolumechange onwaiting onCanPlay onCanPlayThrough onDurationChange
<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} />    // ✓ works

For all other custom events, use on + the exact event name

Test suite proof: react-integration.test.jsx

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 wrong

If 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

File What it covers
react-integration.test.jsx Behavioral: renders React + dispatches real DOM events in jsdom
must-be-camelcase.test.js Rule: no-undercased-react-event-on-custom-element
must-be-lowercase.test.js Rule: no-camelcase-nondelegated-event-on-custom-element
must-be-passthrough.test.js Rule: no-suspicious-camelcase-event-on-custom-element
overlapping-cases.test.js Cross-rule conflict guard
data-integrity.test.js Structural assertions on the generated JSON data files
npm install
npm test

ESLint Rules

no-undercased-react-event-on-custom-element

Rule tests: must-be-camelcase.test.js

Flags lowercase on* props that match a known React delegated event — these should be camelCase. Auto-fixable.

// ✗ ERROR (auto-fixed)
<my-el ondrag={handler} />        // → onDrag
<my-el onclick={handler} />       // → onClick
<my-el onchange={handler} />      // → onChange
<my-el ondoubleclick={handler} /> // → onDoubleClick

no-camelcase-nondelegated-event-on-custom-element

Rule tests: must-be-lowercase.test.js

Flags camelCase props for events that React never wires on custom elements — these must be lowercase. Auto-fixable.

// ✗ ERROR (auto-fixed)
<my-el onClose={handler} />  // → onclose
<my-el onCancel={handler} /> // → oncancel
<my-el onPlay={handler} />   // → onplay
<my-el onLoad={handler} />   // → onload

no-suspicious-camelcase-event-on-custom-element

Rule tests: must-be-passthrough.test.js

Warns when an unknown camelCased on* prop is used on a custom element — the on prefix is stripped verbatim, so onMyEvent listens for MyEvent (capital M), which is almost certainly wrong. Provides a suggestion (not an autofix) to lowercase the first letter.

// ⚠ WARNING (suggestion)
<my-el onMyWidgetToggle={handler} /> // → onmyWidgetToggle
<my-el onWidgetClose={handler} />    // → onwidgetClose

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; all React synthetic events (including Capture variants)
    nonDelegatedEventNames.json      — generated; non-delegated events that must stay lowercase
  __tests__/
    react-integration.test.jsx       — behavioral tests (paths 1–3 + non-delegated events in jsdom)
    must-be-camelcase.test.js        — RuleTester: no-undercased-react-event-on-custom-element
    must-be-passthrough.test.js      — RuleTester: no-suspicious-camelcase-event-on-custom-element
    must-be-lowercase.test.js        — RuleTester: no-camelcase-nondelegated-event-on-custom-element
    overlapping-cases.test.js        — cross-rule conflict guard
  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