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"
>