Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

no-unknown-property add more <video> attributes, onMouseMoveCapture and changes to rule documentation #3390

Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion docs/rules/no-unknown-property.md
Expand Up @@ -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
Expand All @@ -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.
30 changes: 24 additions & 6 deletions lib/rules/no-unknown-property.js
Expand Up @@ -36,6 +36,8 @@ const ATTRIBUTE_TAGS_MAP = {
'circle',
'ellipse',
'g',
'line',
'mask',
'path',
'polygon',
'polyline',
Expand All @@ -45,6 +47,7 @@ const ATTRIBUTE_TAGS_MAP = {
'textPath',
'tref',
'tspan',
'use',
// Animation final state
'animate',
'animateColor',
Expand All @@ -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'],
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 = [
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down
79 changes: 76 additions & 3 deletions tests/lib/rules/no-unknown-property.js
Expand Up @@ -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: {
Expand All @@ -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" />;' },
Expand All @@ -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>;',
Expand All @@ -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>;' },
Expand All @@ -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([
{
Expand Down Expand Up @@ -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',
},
},
],
Expand Down