UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

120 lines (118 loc) 5.41 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { JSXElementHelper as JSXElement } from '../../ast-nodes/jsx-element-helper'; import { createLintRule } from '../utils/create-lint-rule'; import { getAffectedPackageConfig } from '../utils/get-affected-package-config'; import { AFFECTED_ATLASKIT_PACKAGES } from './affected-atlaskit-packages'; import { AFFECTED_HTML_ELEMENTS } from './affected-html-elements'; import { UNWANTED_ATLASKIT_ATTRIBUTES } from './unwanted-atlaskit-attributes'; import { UNWANTED_HTML_ATTRIBUTES } from './unwanted-html-attributes'; const isUnwantedHTMLAtrribute = s => { const attrs = UNWANTED_HTML_ATTRIBUTES; return attrs.includes(s); }; const isUnwantedAtlaskitAtrribute = s => { const attrs = UNWANTED_ATLASKIT_ATTRIBUTES; return attrs.includes(s); }; const isUnwantedAttribute = s => isUnwantedHTMLAtrribute(s) || isUnwantedAtlaskitAtrribute(s); const hasUnwantedAttributes = (node, type) => { const attrNames = [...(type === 'html' ? UNWANTED_HTML_ATTRIBUTES : UNWANTED_ATLASKIT_ATTRIBUTES)]; const unwantedAttrs = node.openingElement.attributes.filter(attr => isNodeOfType(attr, 'JSXAttribute')).map(attr => String(attr.name.name)).filter(isUnwantedAttribute).filter(unwantedAttribute => attrNames.includes(unwantedAttribute)); return unwantedAttrs; }; const rule = createLintRule({ meta: { name: 'no-readonly-or-disabled-inputs', type: 'problem', docs: { description: 'Inputs should almost always be interactive. Disabled and read-only inputs can usually be replaced by a more user-friendly design pattern.', recommended: true, severity: 'warn' }, messages: { noDisabled: 'Inputs should almost always be interactive. Instead of disabling an input, consider removing it altogether, as it cannot be used anyway. Disabled inputs are much less accessible than a well labeled and described input with clear error, valid, and/or helper messages.', noReadOnly: 'Inputs should almost always be interactive. Instead of making an input read-only, consider making it normal text. Read-only inputs do the same thing as normal text but appear as interactive elements and add themselves to the tab-order, which can be confusing for users.' } }, create(context) { const localComponentNames = []; return { ImportDeclaration(node) { const source = node.source.value; const affectedPackage = getAffectedPackageConfig(source, AFFECTED_ATLASKIT_PACKAGES); // Ignore non-atlaskit input packages if (!affectedPackage) { return; } if (!node.specifiers.length) { return; } const defaultImport = node.specifiers.filter(spec => spec.type === 'ImportDefaultSpecifier'); const namedImport = node.specifiers.filter(spec => spec.type === 'ImportSpecifier'); const importNames = affectedPackage.importNames; const usesDefaultImport = importNames.includes('default'); const possibleNamedImports = importNames.filter(importName => importName !== 'default'); if (usesDefaultImport && defaultImport.length && defaultImport[0].local) { localComponentNames.push(defaultImport[0].local.name); } else if (possibleNamedImports.length >= 1 && namedImport.length) { namedImport.forEach(imp => { if (imp.type === 'ImportSpecifier' && 'name' in imp.imported && possibleNamedImports.includes(imp.imported.name)) { localComponentNames.push(imp.local.name); } }); } }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return false; } const elName = JSXElement.getName(node); if (!elName) { return false; } const fixes = []; // If it is one of the affected native HTML elements if (AFFECTED_HTML_ELEMENTS.includes(elName)) { const unwantedAttributes = hasUnwantedAttributes(node, 'html'); if (unwantedAttributes.includes('disabled')) { fixes.push(context.report({ node: node.openingElement, messageId: 'noDisabled' })); } if (unwantedAttributes.includes('readonly')) { fixes.push(context.report({ node: node.openingElement, messageId: 'noReadOnly' })); } // Else, it is a React component } else { // if none of the affected packages is imported, return if (localComponentNames.length === 0) { return; } // if component name is not in the list, exit if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier') || !localComponentNames.includes(node.openingElement.name.name)) { return; } const unwantedAttributes = hasUnwantedAttributes(node, 'atlaskit'); if (unwantedAttributes.includes('isDisabled')) { fixes.push(context.report({ node: node.openingElement, messageId: 'noDisabled' })); } if (unwantedAttributes.includes('isReadOnly')) { fixes.push(context.report({ node: node.openingElement, messageId: 'noReadOnly' })); } } return fixes; } }; } }); export default rule;