UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

144 lines (132 loc) 7.13 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ruleName = exports.messageOutsideForm = exports.messageInsideForm = exports.default = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _eslintCodemodUtils = require("eslint-codemod-utils"); var _jsxElementHelper = require("../../ast-nodes/jsx-element-helper"); var _createLintRule = require("../utils/create-lint-rule"); var _getFormImportLocalNames = require("../utils/get-form-import-local-names"); var _isImportFromPackage = require("../utils/is-import-from-package"); var TEXTFIELD_PACKAGE = '@atlaskit/textfield'; var TEXTAREA_PACKAGE = '@atlaskit/textarea'; var messageInsideForm = exports.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)'; var messageOutsideForm = exports.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)'; var ruleName = exports.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 ((0, _eslintCodemodUtils.isNodeOfType)(parent, 'JSXElement') && (0, _eslintCodemodUtils.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 ((0, _eslintCodemodUtils.isNodeOfType)(parent, 'JSXExpressionContainer')) { // Continue traversing up to find the Field/Form current = parent; continue; } // Check if parent is an arrow function (render prop) if ((0, _eslintCodemodUtils.isNodeOfType)(parent, 'ArrowFunctionExpression')) { current = parent; continue; } current = parent; } return false; } var rule = (0, _createLintRule.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 = (0, _getFormImportLocalNames.getFormImportLocalNames)(node); [].concat((0, _toConsumableArray2.default)((_formImports$Form = formImports.Form) !== null && _formImports$Form !== void 0 ? _formImports$Form : []), (0, _toConsumableArray2.default)((_formImports$Field = formImports.Field) !== null && _formImports$Field !== void 0 ? _formImports$Field : []), (0, _toConsumableArray2.default)((_formImports$Characte = formImports.CharacterCounterField) !== null && _formImports$Characte !== void 0 ? _formImports$Characte : []), (0, _toConsumableArray2.default)((_formImports$Checkbox = formImports.CheckboxField) !== null && _formImports$Checkbox !== void 0 ? _formImports$Checkbox : []), (0, _toConsumableArray2.default)((_formImports$RangeFie = formImports.RangeField) !== null && _formImports$RangeFie !== void 0 ? _formImports$RangeFie : [])).forEach(function (localName) { return formComponentNames.add(localName); }); // Track Textfield and Textarea imports if ((0, _isImportFromPackage.isImportFromPackage)(source, TEXTFIELD_PACKAGE) || (0, _isImportFromPackage.isImportFromPackage)(source, TEXTAREA_PACKAGE)) { var defaultImport = node.specifiers.find(function (spec) { return (0, _eslintCodemodUtils.isNodeOfType)(spec, 'ImportDefaultSpecifier'); }); if (defaultImport && (0, _eslintCodemodUtils.isNodeOfType)(defaultImport, 'ImportDefaultSpecifier')) { var localName = defaultImport.local.name; importedComponents.set(localName, { localName: localName, package: source }); } } }, JSXElement: function JSXElement(node) { if (!(0, _eslintCodemodUtils.isNodeOfType)(node, 'JSXElement')) { return; } if (!(0, _eslintCodemodUtils.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.JSXElementHelper.getAttributeByName(node, 'maxLength'); var hasMinLength = _jsxElementHelper.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 var _default = exports.default = rule;