JSPM

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

1.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 131
  • Score
    100M100P100Q108873F
  • 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 that illustrates how React 19 routes event handler props on custom elements through three different code paths.

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-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-suspicious-camelcase-event-on-custom-element warn suggestion onMyCamelCasedEventonmyCamelCasedEvent (likely wrong casing)

Versioning & event name list

Both rules read from src/data/reactEventNames.json, a committed mapping of lowercase prop names to canonical camelCase names (e.g. "ondrag""onDrag"). It's generated by parsing the installed react-dom development bundle (npm run generate).

A scheduled GitHub Action checks for new React releases daily, regenerates the list, runs tests, and publishes a new minor version automatically when the event registry changes. Patch versions are published manually for rule logic fixes.

Requires react-dom >=19.0.0 (declared in peerDependencies).


Background

React 19 treats event props on custom elements (tags with a - in the name) differently depending on whether the prop name is known to React's synthetic event system. The dispatching logic lives in setPropOnCustomElement.

There are three code paths:

1. Explicitly special-cased props (onClick, onScroll, onScrollEnd)

A handful of prop names have hard-coded case branches in setPropOnCustomElement with event-specific wiring (e.g. onClick gets a Mobile Safari workaround, onScroll/onScrollEnd get non-delegated listeners).

2. Known React events (onDrag, onChange, onFocus, etc.)

Prop names that exist in React's registrationNameDependencies registry are recognized by a hasOwnProperty check and handled through React's synthetic event system. The registry is populated at startup from DOMEventProperties.js and its plugin files — this is the dynamic/derived set. Your handler receives a SyntheticEvent.

The lookup is case-sensitive. The registry key is "onDrag" (camelCase), so ondrag (all lowercase) misses the check and falls through to path 3 below. The handler still fires, but it now participates in a completely different dispatch system: stopPropagation() interacts with native DOM bubbling rather than React's synthetic propagation, meaning it can silently break (or fail to break) other React handlers in the tree. The event also bypasses React's priority scheduling (discrete vs. continuous), which can affect Suspense and hydration behavior. The same applies to onclick, onchange, etc.

3. Unknown props → direct addEventListener (oncustomevent)

Everything else falls through to setValueForPropertyOnCustomComponent, which strips the on prefix and passes the remainder verbatim to element.addEventListener(eventName, fn). The handler receives a raw native Event. This is the correct path for truly custom events like oncustomevent.

Note that the event name is case-sensitive and not lowered — the on prefix is stripped and the rest is passed through as-is. If your custom element dispatches myCamelCasedEvent, the correct prop is onmyCamelCasedEvent (lowercase m after on). Writing onMyCamelCasedEvent would listen for MyCamelCasedEvent (capital M) and silently miss the event.


Test Suite

The tests in src/__tests__/custom-element-events.test.jsx illustrate each path with passing assertions.

npm install
npm test
Path Example prop Code path
1 onClick, onScroll Explicit case in setPropOnCustomElement
2 onDrag, onChange registrationNameDependencies lookup → synthetic event system
2 (anti-pattern) ondrag Misses path 2 (case-sensitive), falls to path 3
3 oncustomevent Fall-through → direct addEventListener (native Event)

ESLint Rules

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

Flags lowercase on* props on custom elements that match a known React synthetic event (e.g. ondrag, onclick, onchange). Auto-fixable via eslint --fix.

// ✗ 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} />

What neither rule flags

// ✓ Correct camelCase — known React event, uses synthetic event system
<my-el onDrag={handler} />
<my-el onClick={handler} />
<my-el onChange={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 src/data/reactEventNames.json
src/
  data/
    reactEventNames.json      — generated; 180 mappings (including Capture variants)
  __tests__/
    custom-element-events.test.jsx   — behavioral tests (paths 1–3 + anti-pattern)
    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
  rules/
    no-undercased-react-event-on-custom-element.js
    no-suspicious-camelcase-event-on-custom-element.js
  index.js                    — ESLint plugin export (both rules)