@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
150 lines (139 loc) • 5.84 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { JSXAttribute } from '../../ast-nodes/jsx-attribute';
import { JSXElementHelper } from '../../ast-nodes/jsx-element-helper';
import { createLintRule } from '../utils/create-lint-rule';
import { isImportFromPackage } from '../utils/is-import-from-package';
var TEXTFIELD_PACKAGE = '@atlaskit/textfield';
var TYPE_PROP = 'type';
var AUTOCOMPLETE_PROP = 'autoComplete';
/**
* Mapping of input types to their corresponding autocomplete values
* Can be expanded in the future for other input types (tel, url, etc.)
*/
var TYPE_TO_AUTOCOMPLETE = {
email: 'email',
tel: 'tel',
url: 'url'
};
export var ruleName = __dirname.split('/').slice(-1)[0];
var rule = createLintRule({
meta: {
name: ruleName,
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Enforce that Textfield components with type="email", "tel", or "url" have an appropriate autocomplete value for WCAG 2.2 SC 1.3.5 compliance (Identify Input Purpose).',
recommended: true,
severity: 'warn'
},
messages: {
missingAutocomplete: 'Textfield with type="email", "tel", or "url" should have an appropriate autocomplete value to comply with WCAG 2.2 SC 1.3.5 (Identify Input Purpose).',
noAutocompleteOff: 'Textfield with type="email", "tel", or "url" should not have autocomplete="off". Set it to an appropriate autocomplete value to comply with WCAG 2.2 SC 1.3.5 (Identify Input Purpose).'
}
},
create: function create(context) {
// Track locally imported Textfield component names
var textfieldImportNames = [];
return {
// Track imports from @atlaskit/textfield
ImportDeclaration: function ImportDeclaration(node) {
if (!isImportFromPackage(node.source.value, TEXTFIELD_PACKAGE)) {
return;
}
node.specifiers.forEach(function (spec) {
// Handle: import Textfield from '@atlaskit/textfield'
if (isNodeOfType(spec, 'ImportDefaultSpecifier')) {
textfieldImportNames.push(spec.local.name);
}
// Handle: import { default as Textfield } from '@atlaskit/textfield' (edge case)
if (isNodeOfType(spec, 'ImportSpecifier') && spec.imported.name === 'default') {
textfieldImportNames.push(spec.local.name);
}
});
},
JSXElement: function JSXElement(node) {
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
// Check if this is a Textfield component
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
var componentName = node.openingElement.name.name;
if (!textfieldImportNames.includes(componentName)) {
return;
}
// Check for type prop
var typeProp = JSXElementHelper.getAttributeByName(node, TYPE_PROP);
if (!typeProp) {
return;
}
// Get the type value - only process if it's a static string literal
var typeValue = JSXAttribute.getValue(typeProp);
if (!typeValue || typeValue.type !== 'Literal') {
// Skip dynamic types (e.g., type={inputType})
return;
}
var typeString = String(typeValue.value);
if (!(typeString in TYPE_TO_AUTOCOMPLETE)) {
// Skip if type is not one we're checking
return;
}
var expectedAutocomplete = TYPE_TO_AUTOCOMPLETE[typeString];
// Check for autoComplete prop
var autoCompleteProp = JSXElementHelper.getAttributeByName(node, AUTOCOMPLETE_PROP);
if (!autoCompleteProp) {
// Missing autoComplete entirely
return context.report({
node: node,
messageId: 'missingAutocomplete',
fix: function fix(fixer) {
return JSXElementHelper.addAttribute(node, AUTOCOMPLETE_PROP, expectedAutocomplete, fixer);
}
});
}
// Check the autoComplete value
var autoCompleteValue = JSXAttribute.getValue(autoCompleteProp);
if (!autoCompleteValue) {
// Boolean `autoComplete` has no value node — add the prop. `autoComplete=""` also yields no
// getValue() (shared helper skips falsy literals); we report but do not autofix to avoid
// inserting a duplicate attribute.
return context.report({
node: node,
messageId: 'missingAutocomplete',
fix: function fix(fixer) {
if (!autoCompleteProp.value) {
return JSXElementHelper.addAttribute(node, AUTOCOMPLETE_PROP, expectedAutocomplete, fixer);
}
return null;
}
});
}
// Handle different value types
if (autoCompleteValue.type === 'Literal') {
var valueString = String(autoCompleteValue.value);
// Check for "off"
if (valueString === 'off') {
return context.report({
node: node,
messageId: 'noAutocompleteOff',
fix: function fix(fixer) {
if (autoCompleteProp.value && isNodeOfType(autoCompleteProp.value, 'Literal')) {
return fixer.replaceText(autoCompleteProp.value, "\"".concat(expectedAutocomplete, "\""));
}
return null;
}
});
}
// Valid autoComplete value (e.g. "email")
// If it is an invalid value, it will be caught by the jsx-a11y/autocomplete-valid rule
return;
}
// If it's a dynamic expression (e.g., autoComplete={someVar}), we can't statically analyze it
// so we allow it
}
};
}
});
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
export default rule;