UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

112 lines (111 loc) 5.79 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; import { findProp } from '../utils/find-prop'; const elements = ['AkButton', 'AKButton', 'Button', 'IconButton', 'MenuItem', 'ButtonItem', 'CustomItem', 'CustomThemeButton', 'LoadingButton', 'BreadcrumbsItem']; const elementsIconProps = ['iconBefore', 'iconAfter', 'icon']; const 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(context) { /** * Contains a map of imported icon components from any atlaskit icon package. */ const iconImports = {}; return { ImportDeclaration(node) { const moduleSource = node.source.value; if (typeof moduleSource === 'string' && ['@atlaskit/icon/glyph/'].find(val => moduleSource.startsWith(val)) && node.specifiers.length) { const defaultImport = node.specifiers.find(spec => spec.type === 'ImportDefaultSpecifier'); if (!defaultImport) { return; } const defaultImportName = defaultImport.local.name; iconImports[defaultImportName] = true; } }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } const name = node.openingElement.name.name; if (elements.includes(name)) { // We've found a DS component that might use icons, let's check them. const iconProps = node.openingElement.attributes.filter(attr => 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(prop => { var _prop$value; if (prop.type !== 'JSXAttribute' || (prop === null || prop === void 0 ? void 0 : (_prop$value = prop.value) === null || _prop$value === void 0 ? void 0 : _prop$value.type) !== 'JSXExpressionContainer') { return; } const hasOtherDefinedLabel = !!node.children.length || !!findProp(node, 'aria-label'); const 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. const labelProp = findProp(expression, 'label'); if (labelProp && labelProp.value && labelProp.value.type === 'Literal') { const isEmptyStringLabel = labelProp.value.value === ''; if (hasOtherDefinedLabel && !isEmptyStringLabel) { context.report({ node: labelProp, messageId: 'unneededLabelPropContents', suggest: [{ desc: 'Remove duplicate label contents', fix: fixer => 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. const hasLabelProp = findProp(node, 'label'); let 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 const paramName = node.parent.params[0].name; hasSpreadPropApplied = !!node.openingElement.attributes.find(attribute => 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;