@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
128 lines (126 loc) • 6.34 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _eslintCodemodUtils = require("eslint-codemod-utils");
var _createLintRule = require("../utils/create-lint-rule");
var _findProp = require("../utils/find-prop");
var elements = ['AkButton', 'AKButton', 'Button', 'IconButton', 'MenuItem', 'ButtonItem', 'CustomItem', 'CustomThemeButton', 'LoadingButton', 'BreadcrumbsItem'];
var elementsIconProps = ['iconBefore', 'iconAfter', 'icon'];
var rule = (0, _createLintRule.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 (!(0, _eslintCodemodUtils.isNodeOfType)(node, 'JSXElement')) {
return;
}
if (!(0, _eslintCodemodUtils.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 || !!(0, _findProp.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 = (0, _findProp.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 = (0, _findProp.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 && (0, _eslintCodemodUtils.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;
}
}
}
};
}
});
var _default = exports.default = rule;