Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-moons-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-primer-react': minor
---

Add no-deprecated-props rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ ESLint rules for Primer React
- [a11y-tooltip-interactive-trigger](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-tooltip-interactive-trigger.md)
- [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md)
- [new-css-color-vars](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/new-css-color-vars.md)
- [no-deprecated-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-props.md)
48 changes: 48 additions & 0 deletions docs/rules/no-deprecated-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Rule Details

This rule enforces to use the recommended API (`ActionList.GroupHeading`) component over the deprecated prop (`title` prop on `ActionList.Group`) for ActionList component.

👎 Examples of **incorrect** code for this rule:

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList>
<ActionList.Group title="Group heading">
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```

👍 Examples of **correct** code for this rule:

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h2">Group heading</ActionList.GroupHeading>
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```

```jsx
/* eslint primer-react/no-deprecated-props: "error" */
import {ActionList} from '@primer/react'

const App = () => (
<ActionList role="lisbox">
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Item 1</ActionList.Item>
</ActionList.Group>
</ActionList>
)
```
1 change: 1 addition & 0 deletions src/configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
'primer-react/new-color-css-vars': 'error',
'primer-react/a11y-explicit-heading': 'error',
'primer-react/new-color-css-vars-have-fallback': 'error',
'primer-react/no-deprecated-props': 'warn',
},
settings: {
github: {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
'new-color-css-vars': require('./rules/new-color-css-vars'),
'a11y-explicit-heading': require('./rules/a11y-explicit-heading'),
'new-color-css-vars-have-fallback': require('./rules/new-color-css-vars-have-fallback'),
'no-deprecated-props': require('./rules/no-deprecated-props'),
},
configs: {
recommended: require('./configs/recommended'),
Expand Down
121 changes: 121 additions & 0 deletions src/rules/__tests__/no-deprecated-props.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use strict'

const {RuleTester} = require('eslint')
const rule = require('../no-deprecated-props')

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
})

ruleTester.run('no-deprecated-props', rule, {
valid: [
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading 1</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading 2</ActionList.GroupHeading>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList.Group>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Group>
<ActionList.GroupHeading>Group heading 2</ActionList.GroupHeading>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList.Group>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h3">Group heading</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList role="listbox">
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Item</ActionList.Item>
</ActionList.Group>
<ActionList.Item>Item 2</ActionList.Item>
</ActionList>`,
`import {ActionList} from '@primer/react';
<ActionList role="menu">
<ActionList.Item>Item</ActionList.Item>
<ActionList.Group>
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
<ActionList.Item>Group item</ActionList.Item>
</ActionList.Group>
</ActionList>`,
],
invalid: [
{
code: `<ActionList.Group title="Group heading 1"></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title="Group heading 1" sx={{padding: 2}}></ActionList.Group>`,
output: `<ActionList.Group sx={{padding: 2}}><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group variant="filled" title="Group heading 1"></ActionList.Group>`,
output: `<ActionList.Group variant="filled"><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={titleVariable}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{titleVariable}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={'Title'}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{'Title'}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
{
code: `<ActionList.Group title={condition ? 'Title' : undefined}></ActionList.Group>`,
output: `<ActionList.Group><ActionList.GroupHeading>{condition ? 'Title' : undefined}</ActionList.GroupHeading></ActionList.Group>`,
errors: [
{
messageId: 'titlePropDeprecated',
},
],
},
],
})
62 changes: 62 additions & 0 deletions src/rules/no-deprecated-props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'
const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')

/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Avoid using deprecated `title` prop on `ActionList.Group` component. Use `ActionList.GroupHeading` instead.',
recommended: true,
url: 'https://primer.style/components/action-list/react/beta#actionlistgroupheading',
},
fixable: 'code',
schema: [],
messages: {
titlePropDeprecated: 'The `title` prop is deprecated. Please use `ActionList.GroupHeading` instead.',
},
},
create(context) {
return {
JSXOpeningElement(node) {
const openingElName = getJSXOpeningElementName(node)
if (openingElName !== 'ActionList.Group') {
return
}
const title = getJSXOpeningElementAttribute(node, 'title')
let groupTitle = ''
if (title !== undefined) {
context.report({
node,
messageId: 'titlePropDeprecated',
fix(fixer) {
// Group title is a string literal i.e. title="title"
if (title.value.type === 'Literal') {
groupTitle = title.value.value
// Group title is a JSX expression i.e. title={title}
} else if (title.value.type === 'JSXExpressionContainer') {
groupTitle = context.sourceCode.getText(title.value)
} else {
// we don't provide fix for cases where the title prop is not a string literal or JSX expression
return []
}
const start = title.range[0]
const end = title.range[1]
return [
fixer.removeRange([start - 1, end]), // remove the space before the title as well
fixer.insertTextAfterRange(
[node.range[1], node.range[1]],
`<ActionList.GroupHeading>${groupTitle}</ActionList.GroupHeading>`,
),
]
},
})
}
},
}
},
}