UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

122 lines (121 loc) 6.07 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; import { findProp } from '../utils/find-prop'; var elements = ['AkButton', 'AKButton', 'Button', 'IconButton', 'MenuItem', 'ButtonItem', 'CustomItem', 'CustomThemeButton', 'LoadingButton', 'BreadcrumbsItem']; var elementsIconProps = ['iconBefore', 'iconAfter', 'icon']; var rule = createLintRule({ meta: { name: 'icon-label', type: 'suggestion', docs: { description: 'Enforces accessible usage of icon labels when composed with Atlassian Design System components.', recommended: true, severity: 'warn' }, hasSuggestions: true, messages: { unneededLabelPropContents: 'Label prop should be an empty string to prevent duplicate screen reader announcements. Learn more here: http://go/adsc/icon/usage#accessibility-guidelines.', missingLabelProp: 'Missing label prop. If there is no supplementary text the label should describe what the icon is, else it should be an empty string. Learn more here: http://go/adsc/icon/usage#accessibility-guidelines.', labelPropShouldHaveContents: 'Icon should have a meaningful label describing what the action is. Learn more here: http://go/adsc/icon/usage#accessibility-guidelines.' } }, create: function create(context) { /** * Contains a map of imported icon components from any atlaskit icon package. */ var iconImports = {}; return { ImportDeclaration: function ImportDeclaration(node) { var moduleSource = node.source.value; if (typeof moduleSource === 'string' && ['@atlaskit/icon/glyph/'].find(function (val) { return moduleSource.startsWith(val); }) && node.specifiers.length) { var defaultImport = node.specifiers.find(function (spec) { return spec.type === 'ImportDefaultSpecifier'; }); if (!defaultImport) { return; } var defaultImportName = defaultImport.local.name; iconImports[defaultImportName] = true; } }, JSXElement: function JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } var name = node.openingElement.name.name; if (elements.includes(name)) { // We've found a DS component that might use icons, let's check them. var iconProps = node.openingElement.attributes.filter(function (attr) { return attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && elementsIconProps.includes(attr.name.name); }); if (iconProps.length) { // This found element has icon props, and it has children. Let's make sure any icon usage has an empty string label. iconProps.forEach(function (prop) { var _prop$value; if (prop.type !== 'JSXAttribute' || (prop === null || prop === void 0 || (_prop$value = prop.value) === null || _prop$value === void 0 ? void 0 : _prop$value.type) !== 'JSXExpressionContainer') { return; } var hasOtherDefinedLabel = !!node.children.length || !!findProp(node, 'aria-label'); var expression = prop.value.expression; if (expression.type !== 'JSXElement' || expression.openingElement.name.type !== 'JSXIdentifier') { return; } if (iconImports[expression.openingElement.name.name]) { // We've found an icon from @atlaskit - let's get to work. var labelProp = findProp(expression, 'label'); if (labelProp && labelProp.value && labelProp.value.type === 'Literal') { var isEmptyStringLabel = labelProp.value.value === ''; if (hasOtherDefinedLabel && !isEmptyStringLabel) { context.report({ node: labelProp, messageId: 'unneededLabelPropContents', suggest: [{ desc: 'Remove duplicate label contents', fix: function fix(fixer) { return fixer.replaceText(labelProp.value, '""'); } }] }); } else if (!hasOtherDefinedLabel && isEmptyStringLabel) { context.report({ node: labelProp, messageId: 'labelPropShouldHaveContents' }); } } } }); } } if (iconImports[name]) { // We've found an icon from @atlaskit - let's get to work. var hasLabelProp = findProp(node, 'label'); var hasSpreadPropApplied = false; // Check to see if it's inside an arrow function and the props have been spread // ✅ <IconButton icon={(iconProps) => <StarIcon {...iconProps} />} label="Add to favorites" /> // ❌ <IconButton icon={() => <StarIcon />} label="Add to favorites" /> if (node.parent && isNodeOfType(node.parent, 'ArrowFunctionExpression') && node.parent.params[0] && node.parent.params[0].type === 'Identifier') { // We are using an arrow function, test if the params have been spread onto the icon component var paramName = node.parent.params[0].name; hasSpreadPropApplied = !!node.openingElement.attributes.find(function (attribute) { return attribute.type === 'JSXSpreadAttribute' && attribute.argument.type === 'Identifier' && attribute.argument.name === paramName; }); } if (!hasLabelProp && !hasSpreadPropApplied) { context.report({ node: node.openingElement, messageId: 'missingLabelProp' }); return; } } } }; } }); export default rule;