Repository: jsx-eslint/eslint-plugin-react (js)

Issue:
[7.31.4] `no-unknown-property` throwing on valid props
Related to #3380

I'm seeing the following errors in my project:

```
Unknown property 'onToggle' found
```
`<details>` supports `onToggle`.

```
Invalid property 'fill' found on tag 'path', but it is only allowed on: svg
```
`<path>` supports `fill`.

```
Unknown property 'as' found
```
`<link>` supports `as`.

---

Edit: It looks like there are even more valid use cases that this rule is not covering. I'd suggest reverting the changes to the `no-unknown-property` rule for v7 and releasing it under a new major version. [React Known Props](https://github.com/tcodes0/react-known-props)  can be used to get all the known props, and users can use the `ignore` option on the rule to add other props like [`css`](https://github.com/jsx-eslint/eslint-plugin-react/issues/3384#issuecomment-1236100291).

Pull request:
[Fix] `no-unknown-property` add properties onToggle, fill, as, pointer events, media events and touch events
Closes #3384

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f451e3f..cfddf0d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,14 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 
 ## Unreleased
 
+### Fixed
+* [`no-unknown-property`]: add properties `onToggle`, `fill`, `as`, and pointer events ([#3385][] @sjarva)
+* [`no-unknown-property`]: add `defaultChecked` property ([#3385][] @sjarva)
+* [`no-unknown-property`]: add touch and media event related properties ([#3385][] @sjarva)
+* [`no-unknown-property`]: `children` is always an acceptable prop; iframes have `scrolling`; ; video has `playsInline` ([#3385][] @ljharb)
+
+[#3385]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3385
+
 ## [7.31.4] - 2022.09.04
 
 ### Fixed
diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js
index 2f883af8..a7bd0291 100644
--- a/lib/rules/no-unknown-property.js
+++ b/lib/rules/no-unknown-property.js
@@ -30,9 +30,57 @@ const ATTRIBUTE_TAGS_MAP = {
   checked: ['input'],
   // image is required for SVG support, all other tags are HTML.
   crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
-  fill: ['svg'],
+  fill: [ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
+    // Fill color
+    'altGlyph',
+    'circle',
+    'ellipse',
+    'g',
+    'path',
+    'polygon',
+    'polyline',
+    'rect',
+    'svg',
+    'text',
+    'textPath',
+    'tref',
+    'tspan',
+    // Animation final state
+    'animate',
+    'animateColor',
+    'animateMotion',
+    'animateTransform',
+    'set',
+  ],
   property: ['meta'],
   viewBox: ['svg'],
+  as: ['link'],
+  // Media events allowed only on audio and video tags, see https://github.com/facebook/react/blob/256aefbea1449869620fb26f6ec695536ab453f5/CHANGELOG.md#notable-enhancements
+  onAbort: ['audio', 'video'],
+  onCanPlay: ['audio', 'video'],
+  onCanPlayThrough: ['audio', 'video'],
+  onDurationChange: ['audio', 'video'],
+  onEmptied: ['audio', 'video'],
+  onEncrypted: ['audio', 'video'],
+  onEnded: ['audio', 'video'],
+  onError: ['audio', 'video'],
+  onLoadedData: ['audio', 'video'],
+  onLoadedMetadata: ['audio', 'video'],
+  onLoadStart: ['audio', 'video'],
+  onPause: ['audio', 'video'],
+  onPlay: ['audio', 'video'],
+  onPlaying: ['audio', 'video'],
+  onProgress: ['audio', 'video'],
+  onRateChange: ['audio', 'video'],
+  onSeeked: ['audio', 'video'],
+  onSeeking: ['audio', 'video'],
+  onStalled: ['audio', 'video'],
+  onSuspend: ['audio', 'video'],
+  onTimeUpdate: ['audio', 'video'],
+  onVolumeChange: ['audio', 'video'],
+  onWaiting: ['audio', 'video'],
+  scrolling: ['iframe'],
+  playsInline: ['video'],
 };
 
 const SVGDOM_ATTRIBUTE_NAMES = {
@@ -127,7 +175,7 @@ const DOM_PROPERTY_NAMES_ONE_WORD = [
   // Element specific attributes
   // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
   // To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
-  'accept', 'action', 'allow', 'alt', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
+  'accept', 'action', 'allow', 'alt', 'as', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
   'content', 'coords', 'csp', 'data', 'decoding', 'default', 'defer', 'disabled', 'form',
   'headers', 'height', 'high', 'href', 'icon', 'importance', 'integrity', 'kind', 'label',
   'language', 'loading', 'list', 'loop', 'low', 'max', 'media', 'method', 'min', 'multiple', 'muted',
@@ -149,7 +197,7 @@ const DOM_PROPERTY_NAMES_ONE_WORD = [
   // OpenGraph meta tag attributes
   'property',
   // React specific attributes
-  'ref', 'key',
+  'ref', 'key', 'children',
 ];
 
 const DOM_PROPERTY_NAMES_TWO_WORDS = [
@@ -168,7 +216,7 @@ const DOM_PROPERTY_NAMES_TWO_WORDS = [
   'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
   'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver',
   'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver',
-  'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
+  'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onToggle', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
   'rowSpan', 'srcDoc', 'srcLang', 'srcSet', 'useMap',
   // SVG attributes
   // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
@@ -204,7 +252,11 @@ const DOM_PROPERTY_NAMES_TWO_WORDS = [
   'autoCorrect', // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
   'autoSave', // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
   // React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
-  'className', 'dangerouslySetInnerHTML', 'defaultValue', 'htmlFor', 'onChange', 'suppressContentEditableWarning', 'suppressHydrationWarning',
+  'className', 'dangerouslySetInnerHTML', 'defaultValue', 'defaultChecked', 'htmlFor', 'onChange',
+  'onInvalid', 'onReset', 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'suppressContentEditableWarning', 'suppressHydrationWarning',
+  'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
+  'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange',
+  'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting',
 ];
 
 const DOM_PROPERTIES_IGNORE_CASE = ['charset'];
@@ -226,11 +278,28 @@ const ARIA_PROPERTIES = [
   'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
 ];
 
+const REACT_ON_PROPS = [
+  'onGotPointerCapture',
+  'onLostPointerCapture',
+  'onPointerCancel',
+  'onPointerDown',
+  'onPointerEnter',
+  'onPointerLeave',
+  'onPointerMove',
+  'onPointerOut',
+  'onPointerOver',
+  'onPointerUp',
+];
+
 function getDOMPropertyNames(context) {
   const ALL_DOM_PROPERTY_NAMES = DOM_PROPERTY_NAMES_TWO_WORDS.concat(DOM_PROPERTY_NAMES_ONE_WORD);
   // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
   if (!testReactVersion(context, '>= 16.1.0')) {
-    return ['allowTransparency'].concat(ALL_DOM_PROPERTY_NAMES);
+    return ALL_DOM_PROPERTY_NAMES.concat('allowTransparency');
+  }
+  // these were added in React v16.4.0, see https://reactjs.org/blog/2018/05/23/react-v-16-4.html and https://github.com/facebook/react/pull/12507
+  if (testReactVersion(context, '>= 16.4.0')) {
+    return ALL_DOM_PROPERTY_NAMES.concat(REACT_ON_PROPS);
   }
   return ALL_DOM_PROPERTY_NAMES;
 }


Test patch:
diff --git a/tests/lib/rules/no-unknown-property.js b/tests/lib/rules/no-unknown-property.js
index 7883afd2..d03179ef 100644
--- a/tests/lib/rules/no-unknown-property.js
+++ b/tests/lib/rules/no-unknown-property.js
@@ -48,11 +48,25 @@ ruleTester.run('no-unknown-property', rule, {
     { code: '<img src="cat_keyboard.jpeg" alt="A cat sleeping on a keyboard" />' },
     { code: '<input type="password" required />' },
     { code: '<input ref={this.input} type="radio" />' },
+    { code: '<div children="anything" />' },
+    { code: '<iframe scrolling="?" />' },
     { code: '<input key="bar" type="radio" />' },
     { code: '<button disabled>You cannot click me</button>;' },
     { code: '<svg key="lock" viewBox="box" fill={10} d="d" stroke={1} strokeWidth={2} strokeLinecap={3} strokeLinejoin={4} transform="something" clipRule="else" x1={5} x2="6" y1="7" y2="8"></svg>' },
+    { code: '<g fill="#7B82A0" fillRule="evenodd"></g>' },
     { code: '<meta property="og:type" content="website" />' },
     { code: '<input type="checkbox" checked={checked} disabled={disabled} id={id} onChange={onChange} />' },
+    { code: '<video playsInline />' },
+    {
+      code: '<div allowTransparency="true" />',
+      settings: {
+        react: { version: '16.0.99' },
+      },
+    },
+    // React related attributes
+    { code: '<div onPointerDown={this.onDown} onPointerUp={this.onUp} />' },
+    { code: '<input type="checkbox" defaultChecked={this.state.checkbox} />' },
+    { code: '<div onTouchStart={this.startAnimation} onTouchEnd={this.stopAnimation} onTouchCancel={this.cancel} onTouchMove={this.move} />' },
     // Case ignored attributes, for `charset` discussion see https://github.com/jsx-eslint/eslint-plugin-react/pull/1863
     { code: '<meta charset="utf-8" />;' },
     { code: '<meta charSet="utf-8" />;' },
@@ -82,8 +96,26 @@ ruleTester.run('no-unknown-property', rule, {
     { code: '<script crossOrigin />' },
     { code: '<audio crossOrigin />' },
     { code: '<svg><image crossOrigin /></svg>' },
+    { code: '<details onToggle={this.onToggle}>Some details</details>' },
+    { code: '<path fill="pink" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z"></path>' },
+    { code: '<link as="audio">Audio content</link>' },
+    { code: '<audio onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onError={this.error}></audio>' },
   ]),
   invalid: parsers.all([
+    {
+      code: '<div allowTransparency="true" />',
+      settings: {
+        react: { version: '16.1.0' },
+      },
+      errors: [
+        {
+          messageId: 'unknownProp',
+          data: {
+            name: 'allowTransparency',
+          },
+        },
+      ],
+    },
     {
       code: '<div hasOwnProperty="should not be allowed property"></div>;',
       errors: [
@@ -298,5 +330,63 @@ ruleTester.run('no-unknown-property', rule, {
         },
       ],
     },
+    {
+      code: '<div as="audio" />',
+      errors: [
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'as',
+            tagName: 'div',
+            allowedTags: 'link',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onError={this.error} />',
+      errors: [
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onAbort',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onDurationChange',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onEmptied',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onEnded',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onError',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+      ],
+    },
   ]),
 });