@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
110 lines • 4.34 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { createLintRule } from '../utils/create-lint-rule';
const MODAL_DIALOG_IMPORT_SOURCES = new Set(['@atlaskit/modal-dialog', '@atlaskit/modal-dialog/modal-dialog']);
/**
* Modal entry-point triggers forward modal dialog props via `modalProps`.
* Do not flag top-level `label` on these components — IconButton-based triggers
* use `label` for the trigger button itself.
*/
const MODAL_ENTRY_POINT_TRIGGERS = [{
source: '@atlassian/entry-points/modal-trigger',
namedImport: 'ModalTrigger'
}, {
source: '@atlassian/entry-points/modal-button-trigger',
namedImport: 'ModalButtonTrigger'
}, {
source: '@atlassian/entry-points/modal-dropdown-item-trigger',
namedImport: 'ModalDropdownItemTrigger'
}, {
source: '@atlassian/entry-points/modal-icon-button-trigger',
namedImport: 'ModalIconButtonTrigger'
}, {
source: '@atlassian/entry-points/full-screen-modal-trigger',
namedImport: 'FullScreenModalTrigger'
}];
const hasLabelInObjectExpression = expression => {
if (!isNodeOfType(expression, 'ObjectExpression')) {
return false;
}
return expression.properties.some(property => {
if (!isNodeOfType(property, 'Property') || property.computed) {
return false;
}
if (isNodeOfType(property.key, 'Identifier')) {
return property.key.name === 'label';
}
return isNodeOfType(property.key, 'Literal') && property.key.value === 'label';
});
};
const rule = createLintRule({
meta: {
name: 'no-modal-label',
type: 'suggestion',
docs: {
description: 'Disallows usage of the `label` prop on Atlassian Design System modal dialog and modal entry-point triggers.',
recommended: true,
severity: 'error'
},
messages: {
noModalLabel: 'Do not use the `label` prop on modal dialog. Use the `ModalTitle` component within the `ModalHeader`, or the `titleId` prop from `useModal()` on your heading element instead.'
}
},
create(context) {
const modalDefaultImports = new Set();
const modalEntryPointTriggerImports = new Set();
return {
ImportDeclaration(node) {
const source = String(node.source.value);
if (MODAL_DIALOG_IMPORT_SOURCES.has(source)) {
node.specifiers.forEach(specifier => {
if (isNodeOfType(specifier, 'ImportDefaultSpecifier')) {
modalDefaultImports.add(specifier.local.name);
}
});
return;
}
const supportedImport = MODAL_ENTRY_POINT_TRIGGERS.find(component => component.source === source);
if (!supportedImport) {
return;
}
node.specifiers.forEach(specifier => {
if (isNodeOfType(specifier, 'ImportSpecifier') && isNodeOfType(specifier.imported, 'Identifier') && specifier.imported.name === supportedImport.namedImport) {
modalEntryPointTriggerImports.add(specifier.local.name);
}
});
},
JSXOpeningElement(node) {
if (!isNodeOfType(node, 'JSXOpeningElement')) {
return;
}
if (!isNodeOfType(node.name, 'JSXIdentifier')) {
return;
}
const componentName = node.name.name;
if (modalDefaultImports.has(componentName)) {
const labelProp = node.attributes.find(attribute => isNodeOfType(attribute, 'JSXAttribute') && isNodeOfType(attribute.name, 'JSXIdentifier') && attribute.name.name === 'label');
if (!labelProp) {
return;
}
context.report({
node,
messageId: 'noModalLabel'
});
return;
}
if (!modalEntryPointTriggerImports.has(componentName)) {
return;
}
const modalPropsAttribute = node.attributes.find(attribute => isNodeOfType(attribute, 'JSXAttribute') && isNodeOfType(attribute.name, 'JSXIdentifier') && attribute.name.name === 'modalProps');
if (!modalPropsAttribute || !isNodeOfType(modalPropsAttribute, 'JSXAttribute') || !modalPropsAttribute.value || !isNodeOfType(modalPropsAttribute.value, 'JSXExpressionContainer') || !hasLabelInObjectExpression(modalPropsAttribute.value.expression)) {
return;
}
context.report({
node,
messageId: 'noModalLabel'
});
}
};
}
});
export default rule;