Repository: jsx-eslint/eslint-plugin-react
Pull request #3390 (2022-09-04 14:41:04)

`no-unknown-property` add more `<video>` attributes, `onMouseMoveCapture ` and changes to rule documentation
This PR adds:
- `  autoPictureInPicture`, `controls`, `controlList`, `disablePictureInPicture`, `disableRemotePlayback`, `loop`, `muted`, `poster`,`preload` and allows them only on `<video>` element
- allows `<line>`, `<mask>` and `<use>` to have `fill` property
- allows `onError` property on `<link>` and `<source>` elements
- allows `onLoad` property on `<link>` element
- allows numbers in valid `data-*` properties
- moves `allowFullscreen` to case ignored properties
- unit tests for these properties
- adds a mention to `no-unknown-property` rule documentation about ignored properties, when using libraries that pass information with some new props (like `css` prop when using `emotion`)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f627147..bae48b41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,18 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 
 ### Fixed
 * [`no-unknown-property`]: `onError` and `onLoad` both work on `img` and `script` ([#3388][] @ljharb)
+* [`no-unknown-property`]: data-* attributes can have numbers ([#3390][] @sjarva)
+* [`no-unknown-property`]: add more audio/video attributes ([#3390][] @sjarva)
+* [`no-unknown-property`]: move allowfullscreen to case ignored attributes ([#3390][] @sjarva)
+* [`no-unknown-property`]: fill works on line, mask, and use elements ([#3390][] @sjarva)
+* [`no-unknown-property`]: add onMouseMoveCapture as valid react-specific attribute ([#3390][] @sjarva)
+* [`no-unknown-property`]: make onLoad and onError be accepted on more elements ([#3390][] @sjarva)
 
+### Changed
+
+* [Docs] [`no-unknown-property`]: add a mention about using ignores properties with libraries that add props ([#3390][] @sjarva)
+
+[#3390]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3390
 [#3388]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3388
 
 ## [7.31.5] - 2022.09.03
diff --git a/docs/rules/no-unknown-property.md b/docs/rules/no-unknown-property.md
index 108fa42a..d142def8 100644
--- a/docs/rules/no-unknown-property.md
+++ b/docs/rules/no-unknown-property.md
@@ -43,7 +43,6 @@ var AnotherComponent = <Foo.bar for="bar" />;
 // Custom web components are ignored
 var MyElem = <div class="foo" is="my-elem"></div>;
 var AtomPanel = <atom-panel class="foo"></atom-panel>;
-
 ```
 
 ## Rule Options
@@ -57,6 +56,23 @@ var AtomPanel = <atom-panel class="foo"></atom-panel>;
 - `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
 - `ignore`: optional array of property and attribute names to ignore during validation.
 
+If you are using a library that passes something as a prop to JSX elements, it is recommended to add those props to the ignored properties.
+
+For example, if you use [emotion](https://emotion.sh/docs/introduction) and its [`css` prop](https://emotion.sh/docs/css-prop)),
+add the following to your `.eslintrc` config file:
+
+```js
+...
+"react/no-unknown-property": ['error', { ignore: ['css'] }]
+...
+```
+
+Now, the following code passes:
+
+```jsx
+var StyledDiv = <div css={{ color: 'pink' }}></div>;
+```
+
 ## When Not To Use It
 
 If you are not using JSX you can disable this rule.
diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js
index 2867c5d5..25a8ece9 100644
--- a/lib/rules/no-unknown-property.js
+++ b/lib/rules/no-unknown-property.js
@@ -36,6 +36,8 @@ const ATTRIBUTE_TAGS_MAP = {
     'circle',
     'ellipse',
     'g',
+    'line',
+    'mask',
     'path',
     'polygon',
     'polyline',
@@ -45,6 +47,7 @@ const ATTRIBUTE_TAGS_MAP = {
     'textPath',
     'tref',
     'tspan',
+    'use',
     // Animation final state
     'animate',
     'animateColor',
@@ -63,8 +66,8 @@ const ATTRIBUTE_TAGS_MAP = {
   onEmptied: ['audio', 'video'],
   onEncrypted: ['audio', 'video'],
   onEnded: ['audio', 'video'],
-  onError: ['audio', 'video', 'img', 'script'],
-  onLoad: ['script', 'img'],
+  onError: ['audio', 'video', 'img', 'link', 'source', 'script'],
+  onLoad: ['script', 'img', 'link'],
   onLoadedData: ['audio', 'video'],
   onLoadedMetadata: ['audio', 'video'],
   onLoadStart: ['audio', 'video'],
@@ -82,6 +85,16 @@ const ATTRIBUTE_TAGS_MAP = {
   onWaiting: ['audio', 'video'],
   scrolling: ['iframe'],
   playsInline: ['video'],
+  // Video related attributes
+  autoPictureInPicture: ['video'],
+  controls: ['audio', 'video'],
+  controlList: ['video'],
+  disablePictureInPicture: ['video'],
+  disableRemotePlayback: ['audio', 'video'],
+  loop: ['audio', 'video'],
+  muted: ['audio', 'video'],
+  poster: ['video'],
+  preload: ['audio', 'video'],
 };
 
 const SVGDOM_ATTRIBUTE_NAMES = {
@@ -199,6 +212,8 @@ const DOM_PROPERTY_NAMES_ONE_WORD = [
   'property',
   // React specific attributes
   'ref', 'key', 'children',
+  // Video specific
+  'controls',
 ];
 
 const DOM_PROPERTY_NAMES_TWO_WORDS = [
@@ -209,7 +224,7 @@ const DOM_PROPERTY_NAMES_TWO_WORDS = [
   // 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
-  'acceptCharset', 'allowFullScreen', 'autoComplete', 'autoPlay', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
+  'acceptCharset', 'autoComplete', 'autoPlay', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
   'colSpan', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
   'frameBorder', 'hrefLang', 'httpEquiv', 'isMap', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
   'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',
@@ -258,9 +273,12 @@ const DOM_PROPERTY_NAMES_TWO_WORDS = [
   'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
   'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange',
   'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting',
+  'onMouseMoveCapture',
+  // Video specific,
+  'autoPictureInPicture', 'controlList', 'disablePictureInPicture', 'disableRemotePlayback',
 ];
 
-const DOM_PROPERTIES_IGNORE_CASE = ['charset'];
+const DOM_PROPERTIES_IGNORE_CASE = ['charset', 'allowfullscreen'];
 
 const ARIA_PROPERTIES = [
   // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
@@ -338,14 +356,14 @@ function isValidHTMLTagInJSX(childNode) {
 
 /**
  * Checks if an attribute name is a valid `data-*` attribute:
- * if the name starts with "data-" and has some lowcase (a to z) words, separated but hyphens (-)
+ * if the name starts with "data-" and has some lowcase (a to z) words that can contain numbers, separated but hyphens (-)
  * (which is also called "kebab case" or "dash case"), then the attribute is valid data attribute.
  *
  * @param {String} name - Attribute name to be tested
  * @returns {boolean} Result
  */
 function isValidDataAttribute(name) {
-  const dataAttrConvention = /^data(-[a-z]*)*$/;
+  const dataAttrConvention = /^data(-[a-z1-9]*)*$/;
   return !!dataAttrConvention.test(name);
 }
 


Test patch:
diff --git a/tests/lib/rules/no-unknown-property.js b/tests/lib/rules/no-unknown-property.js
index d25c3d7c..53120035 100644
--- a/tests/lib/rules/no-unknown-property.js
+++ b/tests/lib/rules/no-unknown-property.js
@@ -54,11 +54,14 @@ ruleTester.run('no-unknown-property', rule, {
     { 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: '<mask fill="#7B82A0"></mask>' },
     { code: '<meta property="og:type" content="website" />' },
     { code: '<input type="checkbox" checked={checked} disabled={disabled} id={id} onChange={onChange} />' },
     { code: '<video playsInline />' },
     { code: '<img onError={foo} onLoad={bar} />' },
     { code: '<script onLoad={bar} onError={foo} />' },
+    { code: '<source onError={foo} />' },
+    { code: '<link onLoad={bar} onError={foo} />' },
     {
       code: '<div allowTransparency="true" />',
       settings: {
@@ -68,7 +71,7 @@ ruleTester.run('no-unknown-property', rule, {
     // 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} />' },
+    { code: '<div onTouchStart={this.startAnimation} onTouchEnd={this.stopAnimation} onTouchCancel={this.cancel} onTouchMove={this.move} onMouseMoveCapture={this.capture} />' },
     // 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" />;' },
@@ -81,6 +84,7 @@ ruleTester.run('no-unknown-property', rule, {
     { code: '<div data-foo-bar="baz"></div>;' },
     { code: '<div data-parent="parent"></div>;' },
     { code: '<div data-index-number="1234"></div>;' },
+    { code: '<div data-e2e-id="5678"></div>;' },
     // Ignoring should work
     {
       code: '<div class="bar"></div>;',
@@ -90,6 +94,10 @@ ruleTester.run('no-unknown-property', rule, {
       code: '<div someProp="bar"></div>;',
       options: [{ ignore: ['someProp'] }],
     },
+    {
+      code: '<div css={{flex: 1}}></div>;',
+      options: [{ ignore: ['css'] }],
+    },
 
     // aria-* attributes should work
     { code: '<button aria-haspopup="true">Click me to open pop up</button>;' },
@@ -100,8 +108,10 @@ ruleTester.run('no-unknown-property', rule, {
     { 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: '<line fill="pink" x1="0" y1="80" x2="100" y2="20"></line>' },
     { 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>' },
+    { code: '<video controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true}></video>' },
+    { code: '<audio controls={this.controls} crossOrigin="anonymous" disableRemotePlayback loop muted preload="none" src="something" onAbort={this.abort} onDurationChange={this.durationChange} onEmptied={this.emptied} onEnded={this.end} onError={this.error}></audio>' },
   ]),
   invalid: parsers.all([
     {
@@ -385,7 +395,70 @@ ruleTester.run('no-unknown-property', rule, {
           data: {
             name: 'onError',
             tagName: 'div',
-            allowedTags: 'audio, video, img, script',
+            allowedTags: 'audio, video, img, link, source, script',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div onLoad={this.load} />',
+      errors: [
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'onLoad',
+            tagName: 'div',
+            allowedTags: 'script, img, link',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div fill="pink" />',
+      errors: [
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'fill',
+            tagName: 'div',
+            allowedTags: 'altGlyph, circle, ellipse, g, line, mask, path, polygon, polyline, rect, svg, text, textPath, tref, tspan, use, animate, animateColor, animateMotion, animateTransform, set',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div controls={this.controls} loop={true} muted={false} src={this.videoSrc} playsInline={true} allowFullScreen></div>',
+      errors: [
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'controls',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'loop',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'muted',
+            tagName: 'div',
+            allowedTags: 'audio, video',
+          },
+        },
+        {
+          messageId: 'invalidPropOnTag',
+          data: {
+            name: 'playsInline',
+            tagName: 'div',
+            allowedTags: 'video',
           },
         },
       ],