UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

137 lines (126 loc) 6.34 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { isNodeOfType } from 'eslint-codemod-utils'; import { JSXElementHelper } from '../../ast-nodes/jsx-element-helper'; import { createLintRule } from '../utils/create-lint-rule'; import { getFormImportLocalNames } from '../utils/get-form-import-local-names'; import { isImportFromPackage } from '../utils/is-import-from-package'; var TEXTFIELD_PACKAGE = '@atlaskit/textfield'; var TEXTAREA_PACKAGE = '@atlaskit/textarea'; export var messageInsideForm = 'When using `maxLength` or `minLength` props with Textfield/Textarea inside a Form, use `CharacterCounterField` from `@atlaskit/form` instead of Field and remove the props from the Textfield/Textarea. This ensures accessibility through real time feedback and aligns with the design system. Read more about [character counter fields](https://atlassian.design/components/form/examples#charactercounterfield)'; export var messageOutsideForm = 'When using `maxLength` or `minLength` props with Textfield/Textarea, use `CharacterCounter` from `@atlaskit/form` alongside your input for real-time character count feedback. This ensures accessibility through screen reader announcements and visual feedback. Read more about [standalone character counter](https://atlassian.design/components/form/examples#standalone-charactercounter)'; export var ruleName = __dirname.split('/').slice(-1)[0]; /** * Check if a node is inside a Form component or Field component */ function isInsideFormOrField(node, formComponentNames) { var current = node; // Traverse up the AST to find parent JSX elements while (current) { var parent = current.parent; if (!parent) { break; } // Check if parent is a JSXElement with a name we're tracking if (isNodeOfType(parent, 'JSXElement') && isNodeOfType(parent.openingElement.name, 'JSXIdentifier')) { var parentName = parent.openingElement.name.name; // Check if this is a Form component or Field component if (formComponentNames.has(parentName)) { return true; } } // Check if parent is inside a JSXExpressionContainer (render prop pattern) // This handles: <Field>{({ fieldProps }) => <Textfield {...fieldProps} />}</Field> if (isNodeOfType(parent, 'JSXExpressionContainer')) { // Continue traversing up to find the Field/Form current = parent; continue; } // Check if parent is an arrow function (render prop) if (isNodeOfType(parent, 'ArrowFunctionExpression')) { current = parent; continue; } current = parent; } return false; } var rule = createLintRule({ meta: { name: ruleName, type: 'suggestion', docs: { description: 'Suggests using CharacterCounterField or CharacterCounter when Textfield or Textarea components have maxLength or minLength props.', recommended: true, severity: 'warn' }, messages: { useCharacterCounterField: messageInsideForm, useCharacterCounter: messageOutsideForm } }, create: function create(context) { // Track imported component names var importedComponents = new Map(); var formComponentNames = new Set(); return { ImportDeclaration: function ImportDeclaration(node) { var _formImports$Form, _formImports$Field, _formImports$Characte, _formImports$Checkbox, _formImports$RangeFie; var source = node.source.value; if (typeof source !== 'string') { return; } // Track Form package imports (Form, Field, etc.) var formImports = getFormImportLocalNames(node); [].concat(_toConsumableArray((_formImports$Form = formImports.Form) !== null && _formImports$Form !== void 0 ? _formImports$Form : []), _toConsumableArray((_formImports$Field = formImports.Field) !== null && _formImports$Field !== void 0 ? _formImports$Field : []), _toConsumableArray((_formImports$Characte = formImports.CharacterCounterField) !== null && _formImports$Characte !== void 0 ? _formImports$Characte : []), _toConsumableArray((_formImports$Checkbox = formImports.CheckboxField) !== null && _formImports$Checkbox !== void 0 ? _formImports$Checkbox : []), _toConsumableArray((_formImports$RangeFie = formImports.RangeField) !== null && _formImports$RangeFie !== void 0 ? _formImports$RangeFie : [])).forEach(function (localName) { return formComponentNames.add(localName); }); // Track Textfield and Textarea imports if (isImportFromPackage(source, TEXTFIELD_PACKAGE) || isImportFromPackage(source, TEXTAREA_PACKAGE)) { var defaultImport = node.specifiers.find(function (spec) { return isNodeOfType(spec, 'ImportDefaultSpecifier'); }); if (defaultImport && isNodeOfType(defaultImport, 'ImportDefaultSpecifier')) { var localName = defaultImport.local.name; importedComponents.set(localName, { localName: localName, package: source }); } } }, JSXElement: function JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } var elementName = node.openingElement.name.name; // Check if this element is one of our tracked components var componentInfo = importedComponents.get(elementName); if (!componentInfo) { return; } // Check if the component has maxLength or minLength props var hasMaxLength = JSXElementHelper.getAttributeByName(node, 'maxLength'); var hasMinLength = JSXElementHelper.getAttributeByName(node, 'minLength'); if (hasMaxLength || hasMinLength) { // Use different messages based on whether inside Form context or not if (isInsideFormOrField(node, formComponentNames)) { context.report({ node: node, messageId: 'useCharacterCounterField' }); } else { context.report({ node: node, messageId: 'useCharacterCounter' }); } } } }; } }); // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports export default rule;