diff --git a/.eslintplugin.js b/.eslintplugin.js index 98ae781dd92..2b3949ff08f 100644 --- a/.eslintplugin.js +++ b/.eslintplugin.js @@ -2,4 +2,5 @@ exports.rules = { i18n: require('./scripts/eslint-plugin/i18n'), 'href-with-rel': require('./scripts/eslint-plugin/rel'), 'require-license-header': require('./scripts/eslint-plugin/require_license_header'), + 'forward-ref': require('./scripts/eslint-plugin/forward_ref_display_name'), }; diff --git a/.eslintrc.js b/.eslintrc.js index e6390e7d5fa..ebdbaa72c6f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,6 +58,7 @@ module.exports = { "prefer-template": "error", "local/i18n": "error", "local/href-with-rel": "error", + "local/forward-ref": "error", "local/require-license-header": [ 'warn', { diff --git a/CHANGELOG.md b/CHANGELOG.md index 7873f18ccfd..90d6535e218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added exports for `EuiSteps` and related components types ([#3471](https://github.com/elastic/eui/pull/3471)) +- Added `displayName` to components using `React.forwardRef` ([#3451](https://github.com/elastic/eui/pull/3451)) ## [`24.0.0`](https://github.com/elastic/eui/tree/v24.0.0) diff --git a/scripts/eslint-plugin/forward_ref_display_name.js b/scripts/eslint-plugin/forward_ref_display_name.js new file mode 100644 index 00000000000..918a4df95ab --- /dev/null +++ b/scripts/eslint-plugin/forward_ref_display_name.js @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Enforce display name to forwardRef components', + }, + }, + create: function(context) { + const forwardRefUsages = []; + const displayNameUsages = []; + return { + VariableDeclarator(node) { + if (node.init && node.init.type === 'CallExpression') { + if ( + node.init.callee && + node.init.callee.type === 'MemberExpression' + ) { + if ( + node.init.callee.property && + node.init.callee.property.name === 'forwardRef' + ) { + forwardRefUsages.push(node.id); + } + } + if (node.init.callee && node.init.callee.name === 'forwardRef') { + forwardRefUsages.push(node.id); + } + } + }, + MemberExpression(node) { + const { property } = node; + if ( + property && + property.type === 'Identifier' && + property.name === 'displayName' + ) { + displayNameUsages.push(node.object); + } + }, + 'Program:exit'() { + forwardRefUsages.forEach(identifier => { + if (!isDisplayNameUsed(identifier)) { + context.report({ + node: identifier, + message: 'Forward ref components must use a display name', + }); + } + }); + }, + }; + function isDisplayNameUsed(identifier) { + const node = displayNameUsages.find( + displayName => displayName.name === identifier.name + ); + return !!node; + } + }, +}; diff --git a/scripts/eslint-plugin/forward_ref_display_name.test.js b/scripts/eslint-plugin/forward_ref_display_name.test.js new file mode 100644 index 00000000000..fe1084fcfce --- /dev/null +++ b/scripts/eslint-plugin/forward_ref_display_name.test.js @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import rule from './forward_ref_display_name.js'; +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: 'babel-eslint', +}); + +const valid = [ + `const Component = React.forwardRef(() => {}) + Component.displayName = "EuiBadgeGroup" +`, +]; + +const invalid = [ + { + code: 'const Component = React.forwardRef(() => {})', + errors: [ + { + message: 'Forward ref components must use a display name', + }, + ], + }, +]; + +ruleTester.run('forward_ref_display_name', rule, { + valid, + invalid, +}); diff --git a/src/components/color_picker/color_picker_swatch.tsx b/src/components/color_picker/color_picker_swatch.tsx index fa3d537eebb..a3d0f6edf67 100644 --- a/src/components/color_picker/color_picker_swatch.tsx +++ b/src/components/color_picker/color_picker_swatch.tsx @@ -53,3 +53,5 @@ export const EuiColorPickerSwatch = forwardRef< /> ); }); + +EuiColorPickerSwatch.displayName = 'EuiColorPickerSwatch'; diff --git a/src/components/color_picker/saturation.tsx b/src/components/color_picker/saturation.tsx index 20f491858fe..add0b5cc1ee 100644 --- a/src/components/color_picker/saturation.tsx +++ b/src/components/color_picker/saturation.tsx @@ -208,3 +208,5 @@ export const EuiSaturation = forwardRef( ); } ); + +EuiSaturation.displayName = 'EuiSaturation'; diff --git a/src/components/datagrid/data_grid_header_row.tsx b/src/components/datagrid/data_grid_header_row.tsx index a3138776189..8457c1345d0 100644 --- a/src/components/datagrid/data_grid_header_row.tsx +++ b/src/components/datagrid/data_grid_header_row.tsx @@ -122,4 +122,6 @@ const EuiDataGridHeaderRow = forwardRef< ); }); +EuiDataGridHeaderRow.displayName = 'EuiDataGridHeaderRow'; + export { EuiDataGridHeaderRow }; diff --git a/src/components/form/range/range_slider.tsx b/src/components/form/range/range_slider.tsx index e20166d4469..e524dbe42f8 100644 --- a/src/components/form/range/range_slider.tsx +++ b/src/components/form/range/range_slider.tsx @@ -94,3 +94,5 @@ export const EuiRangeSlider = forwardRef( ); } ); + +EuiRangeSlider.displayName = 'EuiRangeSlider'; diff --git a/src/components/form/range/range_wrapper.tsx b/src/components/form/range/range_wrapper.tsx index 8ec6186eb83..f0a1622f9e7 100644 --- a/src/components/form/range/range_wrapper.tsx +++ b/src/components/form/range/range_wrapper.tsx @@ -46,3 +46,5 @@ export const EuiRangeWrapper = forwardRef( ); } ); + +EuiRangeWrapper.displayName = 'EuiRangeWrapper';