diff --git a/.eslintplugin.js b/.eslintplugin.js index 92202cd32a0..e787a4fd5e8 100644 --- a/.eslintplugin.js +++ b/.eslintplugin.js @@ -1,3 +1,4 @@ exports.rules = { i18n: require('./scripts/eslint-plugin-i18n/i18n'), + 'href-with-rel': require('./scripts/eslint-plugin-rel/rel') }; diff --git a/.eslintrc.js b/.eslintrc.js index 3de8d8369ea..e04f17479e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { rules: { "prefer-template": "error", "local/i18n": "error", + "local/href-with-rel": "error", "no-use-before-define": "off", "quotes": ["warn", "single", "avoid-escape"], diff --git a/CHANGELOG.md b/CHANGELOG.md index 130b8344e5d..ee1aa75127c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fixed `initialSelectedTab` properties used in `EuiDatePopoverContent` ([#3254](https://github.com/elastic/eui/pull/3254)) - Fixed `EuiSideNavItem` overriding custom `className` of item and icon ([#3283](https://github.com/elastic/eui/pull/3283)) - Fixed `EuiFieldSearch` clear button inconsistencies ([#3270](https://github.com/elastic/eui/pull/3270)) +- Fixed components with `href` usage of `rel` ([#3258](https://github.com/elastic/eui/pull/3258)) ## [`22.3.0`](https://github.com/elastic/eui/tree/v22.3.0) diff --git a/scripts/eslint-plugin-rel/rel.js b/scripts/eslint-plugin-rel/rel.js new file mode 100644 index 00000000000..b946246a25f --- /dev/null +++ b/scripts/eslint-plugin-rel/rel.js @@ -0,0 +1,41 @@ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Enforce rel prop if href exists', + }, + }, + create: function(context) { + return { + /** + * Props of any component is defined in ArrowFunctions + * Example: const EuiButton = ({ foo, bar }) => {}; + */ + ArrowFunctionExpression(node) { + // Functional component contains only single argument + if (node.params && node.params.length === 1) { + // Extract object => { foo, bar } + const objectPattern = node.params[0]; + + if (objectPattern.properties && objectPattern.properties.length) { + // Iterate each Object property to find href or rel + let href = -1; + let rel = -1; + objectPattern.properties.forEach((property, index) => { + if (property.key && property.key.name === 'href') href = index; + if (property.key && property.key.name === 'rel') rel = index; + }); + + // Error => If href is preset and rel is not preset + if (href !== -1 && rel === -1) { + context.report({ + node: objectPattern.properties[href], + message: 'Props must contain rel if href is defined', + }); + } + } + } + }, + }; + }, +}; diff --git a/scripts/eslint-plugin-rel/rel.test.js b/scripts/eslint-plugin-rel/rel.test.js new file mode 100644 index 00000000000..b29599983dd --- /dev/null +++ b/scripts/eslint-plugin-rel/rel.test.js @@ -0,0 +1,27 @@ +const rule = require('./rel'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: 'babel-eslint', +}); + +const valid = [ + 'const Component = ({ href, rel }) => {};', + 'const Component = ({ foo, bar, baz}) => {};', +]; + +const invalid = [ + { + code: 'const Component = ({ href }) => {};', + errors: [ + { + message: 'Props must contain rel if href is defined', + }, + ], + }, +]; + +ruleTester.run('href-with-rel', rule, { + valid, + invalid, +}); diff --git a/src/components/badge/__snapshots__/badge.test.tsx.snap b/src/components/badge/__snapshots__/badge.test.tsx.snap index 98dbc15297a..5d729ab67e7 100644 --- a/src/components/badge/__snapshots__/badge.test.tsx.snap +++ b/src/components/badge/__snapshots__/badge.test.tsx.snap @@ -38,12 +38,34 @@ exports[`EuiBadge is rendered 1`] = ` `; +exports[`EuiBadge is rendered with href and rel provided 1`] = ` + + + + Content + + + +`; + exports[`EuiBadge is rendered with href provided 1`] = ` Content diff --git a/src/components/badge/badge.test.tsx b/src/components/badge/badge.test.tsx index ed610de5e0d..46e9c56a8b5 100644 --- a/src/components/badge/badge.test.tsx +++ b/src/components/badge/badge.test.tsx @@ -86,6 +86,21 @@ describe('EuiBadge', () => { expect(component).toMatchSnapshot(); }); + test('is rendered with href and rel provided', () => { + const component = render( + + Content + + ); + + expect(component).toMatchSnapshot(); + }); + describe('props', () => { describe('iconType', () => { it('is rendered', () => { diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx index 9f9bd6d9ce3..51b89f2f3cb 100644 --- a/src/components/badge/badge.tsx +++ b/src/components/badge/badge.tsx @@ -9,7 +9,11 @@ import React, { import classNames from 'classnames'; import { CommonProps, ExclusiveUnion, keysOf, PropsOf } from '../common'; import chroma from 'chroma-js'; -import { euiPaletteColorBlindBehindText, isValidHex } from '../../services'; +import { + euiPaletteColorBlindBehindText, + isValidHex, + getSecureRelForTarget, +} from '../../services'; import { EuiInnerText } from '../inner_text'; import { EuiIcon, IconColor, IconType } from '../icon'; @@ -30,6 +34,7 @@ type WithButtonProps = { type WithAnchorProps = { href: string; target?: string; + rel?: string; } & Omit, 'href' | 'color'>; type WithSpanProps = Omit, 'onClick' | 'color'>; @@ -118,6 +123,7 @@ export const EuiBadge: FunctionComponent = ({ iconOnClickAriaLabel, closeButtonProps, href, + rel, target, ...rest }) => { @@ -187,6 +193,7 @@ export const EuiBadge: FunctionComponent = ({ const relObj: { href?: string; target?: string; + rel?: string; onClick?: | ((event: React.MouseEvent) => void) | ((event: React.MouseEvent) => void); @@ -195,6 +202,7 @@ export const EuiBadge: FunctionComponent = ({ if (href && !isDisabled) { relObj.href = href; relObj.target = target; + relObj.rel = getSecureRelForTarget({ href, target, rel }); } else if (onClick) { relObj.onClick = onClick; } diff --git a/src/components/header/__snapshots__/header_logo.test.tsx.snap b/src/components/header/__snapshots__/header_logo.test.tsx.snap index e2b9a785ce8..1e7a1218bda 100644 --- a/src/components/header/__snapshots__/header_logo.test.tsx.snap +++ b/src/components/header/__snapshots__/header_logo.test.tsx.snap @@ -5,6 +5,7 @@ exports[`EuiHeaderLogo is rendered 1`] = ` aria-label="aria-label" class="euiHeaderLogo testClass1 testClass2" data-test-subj="test subject string" + rel="noreferrer" >