@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
144 lines (132 loc) • 7.13 kB
JavaScript
"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;