COmpoeentes standardization 2
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
* 2. no-raw-input - Use <InputField> from components/form/input/InputField
|
||||
* 3. no-raw-select - Use <Select> from components/form/Select
|
||||
* 4. no-raw-textarea - Use <TextArea> from components/form/input/TextArea
|
||||
* 5. no-icon-children - Use startIcon/endIcon props instead of icon children in Button
|
||||
*
|
||||
* USAGE: Import in eslint.config.js and enable rules
|
||||
*/
|
||||
@@ -226,5 +227,144 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Disallow icon components as direct children of Button, ButtonGroupItem
|
||||
* Icons must be passed via startIcon or endIcon props to ensure proper horizontal layout.
|
||||
* When icons are children, they get wrapped in spans causing vertical stacking.
|
||||
*
|
||||
* WRONG: <Button><PlusIcon />Add Item</Button> -- icon appears ABOVE text
|
||||
* RIGHT: <Button startIcon={<PlusIcon />}>Add Item</Button> -- icon appears LEFT of text
|
||||
*/
|
||||
'no-icon-children': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow icon components as children of Button. Use startIcon/endIcon props instead.',
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
useIconProps: 'Icon "{{iconName}}" should be passed via startIcon or endIcon prop, not as a child. Icons as children cause vertical stacking. Use: <Button startIcon={<{{iconName}} />}>Text</Button>',
|
||||
useIconPropsGeneric: 'Icons should be passed via startIcon or endIcon prop, not as children. Icons as children cause vertical stacking.',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
// Components that support startIcon/endIcon props
|
||||
const buttonComponents = ['Button', 'ButtonGroupItem'];
|
||||
|
||||
// Pattern to detect icon component names (ends with Icon or is a known icon)
|
||||
const iconPatterns = [
|
||||
/Icon$/, // Any component ending in "Icon" (PlusIcon, RefreshCwIcon, etc.)
|
||||
/^Icon[A-Z]/, // Icon prefix pattern
|
||||
];
|
||||
|
||||
function isIconComponent(name) {
|
||||
return iconPatterns.some(pattern => pattern.test(name));
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
const openingElement = node.openingElement;
|
||||
|
||||
// Check if this is a Button or ButtonGroupItem
|
||||
if (openingElement.name.type !== 'JSXIdentifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentName = openingElement.name.name;
|
||||
if (!buttonComponents.includes(componentName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if startIcon or endIcon props are already provided
|
||||
const hasIconProp = openingElement.attributes.some(attr => {
|
||||
return attr.type === 'JSXAttribute' &&
|
||||
attr.name &&
|
||||
(attr.name.name === 'startIcon' || attr.name.name === 'endIcon' || attr.name.name === 'icon');
|
||||
});
|
||||
|
||||
// If icon props are present, likely the component is correctly configured
|
||||
// Still check children for additional icons
|
||||
|
||||
// Check children for icon components
|
||||
const children = node.children || [];
|
||||
for (const child of children) {
|
||||
// Direct JSX element child
|
||||
if (child.type === 'JSXElement') {
|
||||
const childName = child.openingElement.name;
|
||||
if (childName.type === 'JSXIdentifier' && isIconComponent(childName.name)) {
|
||||
context.report({
|
||||
node: child,
|
||||
messageId: 'useIconProps',
|
||||
data: { iconName: childName.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// JSX expression containing icon (e.g., {loading ? <Spinner /> : <PlusIcon />})
|
||||
if (child.type === 'JSXExpressionContainer') {
|
||||
const expression = child.expression;
|
||||
|
||||
// Direct icon in expression: {<PlusIcon />}
|
||||
if (expression.type === 'JSXElement') {
|
||||
const exprName = expression.openingElement.name;
|
||||
if (exprName.type === 'JSXIdentifier' && isIconComponent(exprName.name)) {
|
||||
context.report({
|
||||
node: expression,
|
||||
messageId: 'useIconProps',
|
||||
data: { iconName: exprName.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional expression: {condition ? <IconA /> : <IconB />}
|
||||
if (expression.type === 'ConditionalExpression') {
|
||||
const consequent = expression.consequent;
|
||||
const alternate = expression.alternate;
|
||||
|
||||
if (consequent.type === 'JSXElement') {
|
||||
const consName = consequent.openingElement.name;
|
||||
if (consName.type === 'JSXIdentifier' && isIconComponent(consName.name)) {
|
||||
context.report({
|
||||
node: consequent,
|
||||
messageId: 'useIconProps',
|
||||
data: { iconName: consName.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (alternate.type === 'JSXElement') {
|
||||
const altName = alternate.openingElement.name;
|
||||
if (altName.type === 'JSXIdentifier' && isIconComponent(altName.name)) {
|
||||
context.report({
|
||||
node: alternate,
|
||||
messageId: 'useIconProps',
|
||||
data: { iconName: altName.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logical expression: {showIcon && <PlusIcon />}
|
||||
if (expression.type === 'LogicalExpression') {
|
||||
const right = expression.right;
|
||||
if (right.type === 'JSXElement') {
|
||||
const rightName = right.openingElement.name;
|
||||
if (rightName.type === 'JSXIdentifier' && isIconComponent(rightName.name)) {
|
||||
context.report({
|
||||
node: right,
|
||||
messageId: 'useIconProps',
|
||||
data: { iconName: rightName.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user