@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
211 lines (200 loc) • 8.51 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { JSXAttribute } from '../../ast-nodes/jsx-attribute';
import { JSXElementHelper } from '../../ast-nodes/jsx-element-helper';
import { createLintRule } from '../utils/create-lint-rule';
const PROP_NAME = 'hasCloseButton';
const MODAL_DIALOG_PACKAGE = '@atlaskit/modal-dialog';
const MODAL_DIALOG_IMPORT_SOURCE = '@atlaskit/modal-dialog/modal-dialog';
const MODAL_HEADER_IMPORT_SOURCE = '@atlaskit/modal-dialog/modal-header';
const CLOSE_BUTTON_IMPORT_SOURCE = '@atlaskit/modal-dialog/close-button';
// Lint rule message
const message = '`hasCloseButton` should be set to `true` or the `CloseButton` component should be used to make modal dialog accessible.';
// Fix messages
export const addHasCloseButtonProp = 'Add `hasCloseButton` prop.';
export const setHasCloseButtonPropToTrue = 'Set `hasCloseButton` prop to `true`.';
export const useCloseButtonOrNewProp = 'Set `hasCloseButton` prop to `true` in `ModalHeader` or use `CloseButton` export if customization is desired.';
export const ruleName = __dirname.split('/').slice(-1)[0];
const rule = createLintRule({
meta: {
name: ruleName,
type: 'suggestion',
fixable: 'code',
hasSuggestions: true,
docs: {
description: "Encourages makers to use close button in Atlassian Design System's modal dialog component.",
recommended: true,
severity: 'warn'
},
messages: {
modalHeaderMissingHasCloseButtonProp: message,
modalHeaderHasCloseButtonPropIsFalse: message,
noCloseButtonExists: message
}
},
create(context) {
// List of component's locally imported names that match
let defaultImportLocalName;
let modalHeaderLocalName;
let closeButtonLocalName;
return {
// Only run rule in files where the package is imported
ImportDeclaration(node) {
node.specifiers.forEach(identifier => {
if ((node.source.value === MODAL_DIALOG_PACKAGE || node.source.value === MODAL_DIALOG_IMPORT_SOURCE) && isNodeOfType(identifier, 'ImportDefaultSpecifier')) {
defaultImportLocalName = identifier.local.name;
} else if (node.source.value === MODAL_HEADER_IMPORT_SOURCE && isNodeOfType(identifier, 'ImportDefaultSpecifier')) {
modalHeaderLocalName = identifier.local.name;
} else if ((node.source.value === MODAL_DIALOG_PACKAGE || node.source.value === CLOSE_BUTTON_IMPORT_SOURCE) && isNodeOfType(identifier, 'ImportSpecifier') && 'name' in identifier.imported) {
const importName = identifier.imported.name;
const localName = identifier.local.name;
if (importName === 'ModalHeader') {
modalHeaderLocalName = localName;
} else if (importName === 'CloseButton') {
closeButtonLocalName = localName;
}
} else if (node.source.value === CLOSE_BUTTON_IMPORT_SOURCE && isNodeOfType(identifier, 'ImportDefaultSpecifier')) {
closeButtonLocalName = identifier.local.name;
}
});
},
JSXElement(node) {
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
const {
name
} = node.openingElement.name;
if (![defaultImportLocalName, modalHeaderLocalName].includes(name)) {
return;
}
let modalHeaderNode = null;
let closeButtonNode = null;
const checkNode = node => {
if (modalHeaderNode && closeButtonNode) {
return;
}
// Add expression container's body if an expression container
if (isNodeOfType(node, 'JSXExpressionContainer')) {
const expression = node.expression;
if (isNodeOfType(expression, 'ArrowFunctionExpression') || isNodeOfType(expression, 'FunctionExpression')) {
if (isNodeOfType(expression.body, 'JSXElement') || isNodeOfType(expression.body, 'JSXFragment')) {
searchExpression(expression.body);
} else if (isNodeOfType(expression.body, 'BlockStatement')) {
expression.body.body.forEach(statement => {
if (isNodeOfType(statement, 'ReturnStatement') && statement.argument) {
searchExpression(statement.argument);
}
});
}
} else if (isNodeOfType(expression, 'LogicalExpression')) {
const {
left,
right
} = expression;
[left, right].forEach(e => {
searchExpression(e);
});
} else {
searchExpression(expression);
}
return;
}
// Skip if not a JSX Element
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
// Skip if opening element is not an identifier
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
// if child is CloseButton, return true
if (node.openingElement.name.name === closeButtonLocalName) {
closeButtonNode = node;
} else if (node.openingElement.name.name === modalHeaderLocalName) {
modalHeaderNode = node;
}
if (node.children) {
searchNode(node);
}
};
const searchExpression = expression => {
if (isNodeOfType(expression, 'JSXElement')) {
searchNode(expression, true);
} else if (isNodeOfType(expression, 'JSXFragment')) {
searchJSXFragment(expression);
}
};
const searchNode = (node, searchSelf = false) => {
if (searchSelf) {
checkNode(node);
}
for (let child of node.children) {
checkNode(child);
}
};
const searchJSXFragment = node => {
if (isNodeOfType(node, 'JSXFragment')) {
for (let child of node.children) {
checkNode(child);
}
}
};
if (name === defaultImportLocalName) {
searchNode(node);
// If there is a close button, skip the rest, as this satisfies the
// rule. If there is a modal header, it will be recognized in later
// scans, so don't add duplicate errors
if (closeButtonNode || modalHeaderNode) {
return;
// No close button or modal header exists
} else {
return context.report({
node,
messageId: 'noCloseButtonExists'
});
}
} else if (name === modalHeaderLocalName) {
modalHeaderNode = node;
searchNode(node);
// If there is a close button, skip the rest, as this satisfies the rule.
if (closeButtonNode) {
return;
}
// No close button exists, so check the modal header
const prop = JSXElementHelper.getAttributeByName(modalHeaderNode, PROP_NAME);
// If the prop exists
if (prop) {
const attrValue = JSXAttribute.getValue(prop);
// If the value is a boolean with value `false`
if ((attrValue === null || attrValue === void 0 ? void 0 : attrValue.type) === 'ExpressionStatement Literal' && (attrValue === null || attrValue === void 0 ? void 0 : attrValue.value) === false) {
return context.report({
node: modalHeaderNode,
messageId: 'modalHeaderHasCloseButtonPropIsFalse',
suggest: [{
desc: setHasCloseButtonPropToTrue,
// Set to true by setting to boolean HTML/JSX attribute
fix: fixer => [fixer.replaceText(prop, PROP_NAME)]
}]
});
}
// If the prop does not exist
} else {
return context.report({
node: modalHeaderNode,
messageId: 'modalHeaderMissingHasCloseButtonProp',
suggest: [{
desc: addHasCloseButtonProp,
fix: fixer => [fixer.insertTextAfter(modalHeaderNode.openingElement.name, ` ${PROP_NAME}`)]
}]
});
}
}
}
};
}
});
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
export default rule;