mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 10:33:31 +00:00
[CL-1058] Eslint suggestions for no-bwi-class and no-icon-children (#19134)
Follow up to #19104 and #18584 to add eslint suggestions that can be applied in editors to speed up resolving the lints. Also adds a fixedWidth input to bit-icon since having fixed width icons is fairly common and I would prefer that we don't keep bwi-fw
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
||||
import {
|
||||
booleanAttribute,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { BitwardenIcon } from "../shared/icon";
|
||||
|
||||
@@ -24,7 +30,12 @@ export class IconComponent {
|
||||
*/
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
protected readonly classList = computed(() => {
|
||||
return ["bwi", this.name()].join(" ");
|
||||
});
|
||||
/**
|
||||
* Whether the icon should use a fixed width
|
||||
*/
|
||||
readonly fixedWidth = input(false, { transform: booleanAttribute });
|
||||
|
||||
protected readonly classList = computed(() =>
|
||||
["bwi", this.name(), this.fixedWidth() && "bwi-fw"].filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ The `bit-icon` component renders Bitwarden Web Icons (bwi) using icon font class
|
||||
## Basic Usage
|
||||
|
||||
```html
|
||||
<bit-icon name="bwi-lock"></bit-icon>
|
||||
<bit-icon name="bwi-lock" />
|
||||
```
|
||||
|
||||
## Icon Names
|
||||
@@ -29,7 +29,7 @@ By default, icons are decorative and marked with `aria-hidden="true"`. To make a
|
||||
provide an `ariaLabel`:
|
||||
|
||||
```html
|
||||
<bit-icon name="bwi-lock" [ariaLabel]="'Secure lock'"></bit-icon>
|
||||
<bit-icon name="bwi-lock" [ariaLabel]="'Secure lock'" />
|
||||
```
|
||||
|
||||
## Styling
|
||||
@@ -38,7 +38,7 @@ The component renders as an inline element. Apply standard CSS classes or styles
|
||||
appearance:
|
||||
|
||||
```html
|
||||
<bit-icon name="bwi-lock" class="tw-text-primary-500 tw-text-2xl"></bit-icon>
|
||||
<bit-icon name="bwi-lock" class="tw-text-primary-500 tw-text-2xl" />
|
||||
```
|
||||
|
||||
## Note on SVG Icons
|
||||
|
||||
@@ -54,6 +54,47 @@ export const WithAriaLabel: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const FixedWidth: Story = {
|
||||
args: {
|
||||
name: "bwi-lock",
|
||||
fixedWidth: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const FixedWidthComparison: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-lock" fixedWidth />
|
||||
<span>bwi-lock (fixed width)</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-eye" fixedWidth />
|
||||
<span>bwi-eye (fixed width)</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-collection" fixedWidth />
|
||||
<span>bwi-collection (fixed width)</span>
|
||||
</div>
|
||||
<hr class="tw-my-2" />
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-lock" />
|
||||
<span>bwi-lock (default)</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-eye" />
|
||||
<span>bwi-eye (default)</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<bit-icon name="bwi-collection" />
|
||||
<span>bwi-collection (default)</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const CompareWithLegacy: Story = {
|
||||
render: () => ({
|
||||
template: `<bit-icon name="bwi-lock"></bit-icon> <i class="bwi bwi-lock"></i>`,
|
||||
|
||||
30
libs/eslint/components/bwi-utils.mjs
Normal file
30
libs/eslint/components/bwi-utils.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
/** Matches any bwi class: the bare "bwi" base class or "bwi-<name>" variants. */
|
||||
export const BWI_CLASS_RE = /\bbwi(?:-[\w-]+)?\b/g;
|
||||
|
||||
/**
|
||||
* Helper / utility classes from libs/angular/src/scss/bwicons/styles/style.scss.
|
||||
* These don't represent icon names and can be used independently.
|
||||
*/
|
||||
export const BWI_HELPER_CLASSES = new Set([
|
||||
"bwi-fw", // Fixed width
|
||||
"bwi-sm", // Small
|
||||
"bwi-lg", // Large
|
||||
"bwi-2x", // 2x size
|
||||
"bwi-3x", // 3x size
|
||||
"bwi-4x", // 4x size
|
||||
"bwi-spin", // Spin animation
|
||||
"bwi-ul", // List
|
||||
"bwi-li", // List item
|
||||
"bwi-rotate-270", // Rotation
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract the icon name from a class string.
|
||||
* Returns the first bwi-* class that is not a helper class and not the bare "bwi" base class.
|
||||
* Returns null if no single icon name can be determined.
|
||||
*/
|
||||
export function extractIconNameFromClassValue(classValue) {
|
||||
const bwiClasses = classValue.match(BWI_CLASS_RE) || [];
|
||||
const iconNames = bwiClasses.filter((cls) => cls !== "bwi" && !BWI_HELPER_CLASSES.has(cls));
|
||||
return iconNames.length === 1 ? iconNames[0] : null;
|
||||
}
|
||||
@@ -1,33 +1,113 @@
|
||||
import { BWI_CLASS_RE, BWI_HELPER_CLASSES, extractIconNameFromClassValue } from "./bwi-utils.mjs";
|
||||
|
||||
export const errorMessage =
|
||||
"Use <bit-icon> component instead of applying 'bwi' classes directly. Example: <bit-icon name=\"bwi-lock\"></bit-icon>";
|
||||
|
||||
// Helper classes from libs/angular/src/scss/bwicons/styles/style.scss
|
||||
// These are utility classes that can be used independently
|
||||
const ALLOWED_BWI_HELPER_CLASSES = new Set([
|
||||
"bwi-fw", // Fixed width
|
||||
"bwi-sm", // Small
|
||||
"bwi-lg", // Large
|
||||
"bwi-2x", // 2x size
|
||||
"bwi-3x", // 3x size
|
||||
"bwi-4x", // 4x size
|
||||
"bwi-spin", // Spin animation
|
||||
"bwi-ul", // List
|
||||
"bwi-li", // List item
|
||||
"bwi-rotate-270", // Rotation
|
||||
]);
|
||||
/**
|
||||
* Parse the class string and return the remaining classes and whether bwi-fw is present.
|
||||
* Drops: "bwi" base class, the icon name class, and "bwi-fw" (mapped to fixedWidth input).
|
||||
* Keeps: other helper bwi classes and non-bwi classes (tw-*, etc.)
|
||||
*/
|
||||
function parseClasses(classValue, iconName) {
|
||||
let hasFixedWidth = false;
|
||||
const remaining = [];
|
||||
for (const cls of classValue.split(/\s+/)) {
|
||||
if (!cls || cls === "bwi" || cls === iconName) continue;
|
||||
if (cls === "bwi-fw") {
|
||||
hasFixedWidth = true;
|
||||
} else {
|
||||
remaining.push(cls);
|
||||
}
|
||||
}
|
||||
return { hasFixedWidth, remainingClasses: remaining.join(" ") };
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description:
|
||||
"Discourage using 'bwi' font icon classes directly in favor of the <bit-icon> component",
|
||||
category: "Best Practices",
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
useBitIcon: errorMessage,
|
||||
replaceBwi: "Replace with <bit-icon>. Note: ensure IconModule is imported in your component.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
/**
|
||||
* Creates a fixer function if the element can be safely auto-fixed.
|
||||
* Only fixes <i> elements with static class attributes and a single extractable icon name.
|
||||
*/
|
||||
function createFix(node, classAttr, classValue) {
|
||||
// Only auto-fix <i> elements
|
||||
if (node.name !== "i") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only fix static class attributes (not [class] or [ngClass] bindings)
|
||||
const isStaticAttr = (node.attributes || []).includes(classAttr);
|
||||
if (!isStaticAttr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the icon name -- bail if none or ambiguous
|
||||
const iconName = extractIconNameFromClassValue(classValue);
|
||||
if (!iconName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't fix if the element has Angular bindings, outputs, or references
|
||||
if (
|
||||
(node.inputs || []).length > 0 ||
|
||||
(node.outputs || []).length > 0 ||
|
||||
(node.references || []).length > 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't fix if the element has non-whitespace children (element nodes or non-empty text)
|
||||
const hasContent = (node.children || []).some(
|
||||
(child) => child.value === undefined || child.value.trim() !== "",
|
||||
);
|
||||
if (hasContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get remaining classes (helpers + non-bwi classes)
|
||||
const { hasFixedWidth, remainingClasses } = parseClasses(classValue, iconName);
|
||||
|
||||
// Collect other attributes to preserve
|
||||
// Drop: class (rebuilt above), aria-hidden="true" (bit-icon handles it automatically)
|
||||
const otherAttrs = (node.attributes || []).filter((attr) => {
|
||||
if (attr.name === "class") return false;
|
||||
if (attr.name === "aria-hidden" && attr.value === "true") return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Build the replacement <bit-icon> element
|
||||
const attrs = [`name="${iconName}"`];
|
||||
if (hasFixedWidth) {
|
||||
attrs.push("fixedWidth");
|
||||
}
|
||||
if (remainingClasses) {
|
||||
attrs.push(`class="${remainingClasses}"`);
|
||||
}
|
||||
for (const attr of otherAttrs) {
|
||||
attrs.push(attr.value != null ? `${attr.name}="${attr.value}"` : attr.name);
|
||||
}
|
||||
const replacement = `<bit-icon ${attrs.join(" ")} />`;
|
||||
|
||||
const start = node.sourceSpan.start.offset;
|
||||
const end = node.sourceSpan.end.offset;
|
||||
|
||||
return (fixer) => fixer.replaceTextRange([start, end], replacement);
|
||||
}
|
||||
|
||||
return {
|
||||
Element(node) {
|
||||
// Get all class-related attributes
|
||||
@@ -45,21 +125,22 @@ export default {
|
||||
}
|
||||
|
||||
// Extract all bwi classes from the class string
|
||||
const bwiClassMatches = classValue.match(/\bbwi(?:-[\w-]+)?\b/g);
|
||||
const bwiClassMatches = classValue.match(BWI_CLASS_RE);
|
||||
|
||||
if (!bwiClassMatches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any bwi class is NOT in the allowed helper classes list
|
||||
const hasDisallowedBwiClass = bwiClassMatches.some(
|
||||
(cls) => !ALLOWED_BWI_HELPER_CLASSES.has(cls),
|
||||
);
|
||||
const hasDisallowedBwiClass = bwiClassMatches.some((cls) => !BWI_HELPER_CLASSES.has(cls));
|
||||
|
||||
if (hasDisallowedBwiClass) {
|
||||
const fix = createFix(node, classAttr, classValue);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: errorMessage,
|
||||
messageId: "useBitIcon",
|
||||
...(fix ? { suggest: [{ messageId: "replaceBwi", fix }] } : {}),
|
||||
});
|
||||
// Only report once per element
|
||||
break;
|
||||
|
||||
@@ -12,11 +12,11 @@ ruleTester.run("no-bwi-class-usage", rule.default, {
|
||||
valid: [
|
||||
{
|
||||
name: "should allow bit-icon component usage",
|
||||
code: `<bit-icon icon="bwi-lock"></bit-icon>`,
|
||||
code: `<bit-icon icon="bwi-lock" />`,
|
||||
},
|
||||
{
|
||||
name: "should allow bit-icon with bwi-fw helper class",
|
||||
code: `<bit-icon icon="bwi-lock" class="bwi-fw"></bit-icon>`,
|
||||
code: `<bit-icon icon="bwi-lock" class="bwi-fw" />`,
|
||||
},
|
||||
{
|
||||
name: "should allow bit-icon with name attribute and bwi-fw helper class",
|
||||
@@ -53,28 +53,108 @@ ruleTester.run("no-bwi-class-usage", rule.default, {
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: "should error on direct bwi class usage",
|
||||
name: "should suggest replacing direct bwi class usage with bit-icon",
|
||||
code: `<i class="bwi bwi-lock"></i>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [{ messageId: "replaceBwi", output: `<bit-icon name="bwi-lock" />` }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should error on bwi class with other classes",
|
||||
name: "should suggest fix preserving non-bwi classes",
|
||||
code: `<i class="tw-flex bwi bwi-lock tw-p-2"></i>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "replaceBwi",
|
||||
output: `<bit-icon name="bwi-lock" class="tw-flex tw-p-2" />`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should error on single bwi-* icon class",
|
||||
name: "should suggest fix for single bwi-* icon class without base bwi",
|
||||
code: `<i class="bwi-lock"></i>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [{ messageId: "replaceBwi", output: `<bit-icon name="bwi-lock" />` }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should error on icon classes even with helper classes",
|
||||
name: "should suggest fix converting bwi-fw to fixedWidth attribute",
|
||||
code: `<i class="bwi bwi-lock bwi-fw"></i>`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "replaceBwi",
|
||||
output: `<bit-icon name="bwi-lock" fixedWidth />`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should not suggest fix for base bwi class alone (no icon name)",
|
||||
code: `<i class="bwi"></i>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
},
|
||||
{
|
||||
name: "should error on base bwi class alone",
|
||||
code: `<i class="bwi"></i>`,
|
||||
name: "should suggest fix dropping aria-hidden='true'",
|
||||
code: `<i class="bwi bwi-lock" aria-hidden="true"></i>`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [{ messageId: "replaceBwi", output: `<bit-icon name="bwi-lock" />` }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should suggest fix preserving other attributes and dropping aria-hidden",
|
||||
code: `<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "replaceBwi",
|
||||
output: `<bit-icon name="bwi-external-link" slot="end" />`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should suggest fix with fixedWidth and preserve tw utility classes",
|
||||
code: `<i class="bwi bwi-plus bwi-fw tw-me-2" aria-hidden="true"></i>`,
|
||||
errors: [
|
||||
{
|
||||
message: errorMessage,
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "replaceBwi",
|
||||
output: `<bit-icon name="bwi-plus" fixedWidth class="tw-me-2" />`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should not suggest fix for non-i elements (div)",
|
||||
code: `<div class="bwi bwi-lock"></div>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
},
|
||||
{
|
||||
name: "should not suggest fix for non-i elements (span)",
|
||||
code: `<span class="bwi bwi-lock"></span>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,20 +1,171 @@
|
||||
import { extractIconNameFromClassValue } from "./bwi-utils.mjs";
|
||||
|
||||
export const errorMessage =
|
||||
'Avoid placing icon elements (<i class="bwi ..."> or <bit-icon>) inside a bitButton or bitLink. ' +
|
||||
"Use the [startIcon] or [endIcon] inputs instead. " +
|
||||
'Example: <button bitButton startIcon="bwi-plus">Label</button>';
|
||||
|
||||
/**
|
||||
* Extract the icon name from a child element.
|
||||
* Supports both <i class="bwi bwi-xxx"> and <bit-icon name="bwi-xxx">.
|
||||
* Returns null if the icon name cannot be determined.
|
||||
*/
|
||||
function extractIconName(child) {
|
||||
if (child.name === "bit-icon") {
|
||||
const nameAttr = (child.attributes || []).find((a) => a.name === "name");
|
||||
return nameAttr && typeof nameAttr.value === "string" ? nameAttr.value : null;
|
||||
}
|
||||
|
||||
if (child.name === "i") {
|
||||
const classAttr = (child.attributes || []).find((a) => a.name === "class");
|
||||
if (!classAttr || typeof classAttr.value !== "string") return null;
|
||||
return extractIconNameFromClassValue(classAttr.value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Classes that can safely be dropped when converting to startIcon/endIcon
|
||||
// because the button component handles the equivalent spacing internally.
|
||||
const DROPPABLE_CLASSES = new Set(["tw-mr-2", "tw-ml-2", "tw-ms-2", "tw-me-2"]);
|
||||
|
||||
/**
|
||||
* Check whether the icon child is simple enough for a clean suggestion.
|
||||
* Returns false if the child has extra classes, attributes, or bindings
|
||||
* that would be lost when converting to startIcon/endIcon.
|
||||
*/
|
||||
function isSimpleIconChild(child) {
|
||||
if ((child.inputs || []).length > 0) return false;
|
||||
|
||||
const hasContent = (child.children || []).some(
|
||||
(c) => c.value === undefined || (typeof c.value === "string" && c.value.trim() !== ""),
|
||||
);
|
||||
if (hasContent) return false;
|
||||
|
||||
if (child.name === "i") {
|
||||
const otherAttrs = (child.attributes || []).filter(
|
||||
(a) => a.name !== "class" && !(a.name === "aria-hidden" && a.value === "true"),
|
||||
);
|
||||
if (otherAttrs.length > 0) return false;
|
||||
|
||||
const classAttr = (child.attributes || []).find((a) => a.name === "class");
|
||||
if (!classAttr) return false;
|
||||
const classes = (classAttr.value || "").split(/\s+/).filter(Boolean);
|
||||
return !classes.some((cls) => !cls.startsWith("bwi") && !DROPPABLE_CLASSES.has(cls));
|
||||
}
|
||||
|
||||
if (child.name === "bit-icon") {
|
||||
const otherAttrs = (child.attributes || []).filter((a) => {
|
||||
if (a.name === "name") return false;
|
||||
if (a.name === "aria-hidden" && a.value === "true") return false;
|
||||
if (a.name === "class") {
|
||||
const classes = (a.value || "").split(/\s+/).filter(Boolean);
|
||||
return classes.some((cls) => !DROPPABLE_CLASSES.has(cls));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return otherAttrs.length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the icon child is at the start or end of the parent's content.
|
||||
* Returns "start", "end", or null (ambiguous).
|
||||
*/
|
||||
function getIconPosition(parent, iconChild) {
|
||||
const children = parent.children || [];
|
||||
const meaningfulChildren = children.filter(
|
||||
(child) => child.name || (typeof child.value === "string" && child.value.trim() !== ""),
|
||||
);
|
||||
|
||||
if (meaningfulChildren.length === 0) return "start";
|
||||
|
||||
const index = meaningfulChildren.indexOf(iconChild);
|
||||
if (index === 0) return "start";
|
||||
if (index === meaningfulChildren.length - 1) return "end";
|
||||
return null;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description:
|
||||
"Discourage using icon child elements inside bitButton; use startIcon/endIcon inputs instead",
|
||||
category: "Best Practices",
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
noIconChildren: errorMessage,
|
||||
useStartIcon: "Replace with startIcon input on the parent element.",
|
||||
useEndIcon: "Replace with endIcon input on the parent element.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
/**
|
||||
* Creates suggestions for replacing an icon child with startIcon/endIcon.
|
||||
* Returns an empty array if the icon cannot be safely converted.
|
||||
*/
|
||||
function createSuggestions(parent, iconChild) {
|
||||
const iconName = extractIconName(iconChild);
|
||||
if (!iconName) return [];
|
||||
|
||||
if (!isSimpleIconChild(iconChild)) return [];
|
||||
|
||||
const parentAttrNames = [
|
||||
...(parent.attributes?.map((a) => a.name) ?? []),
|
||||
...(parent.inputs?.map((i) => i.name) ?? []),
|
||||
];
|
||||
|
||||
const position = getIconPosition(parent, iconChild);
|
||||
|
||||
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
||||
const text = sourceCode.getText();
|
||||
|
||||
const makeFix = (inputName) => (fixer) => {
|
||||
const insertOffset = parent.startSourceSpan.end.offset - 1;
|
||||
|
||||
const {
|
||||
start: { offset: removeStart },
|
||||
end: { offset: removeEnd },
|
||||
} = iconChild.sourceSpan;
|
||||
|
||||
return [
|
||||
fixer.insertTextBeforeRange([insertOffset, insertOffset], ` ${inputName}="${iconName}"`),
|
||||
fixer.removeRange([
|
||||
inputName === "startIcon"
|
||||
? removeStart
|
||||
: removeStart - text.slice(0, removeStart).match(/ *$/)[0].length,
|
||||
inputName === "startIcon"
|
||||
? removeEnd + text.slice(removeEnd).match(/^ */)[0].length
|
||||
: removeEnd,
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
if ((position === "start" || position === null) && !parentAttrNames.includes("startIcon")) {
|
||||
suggestions.push({
|
||||
messageId: "useStartIcon",
|
||||
fix: makeFix("startIcon"),
|
||||
});
|
||||
}
|
||||
|
||||
if ((position === "end" || position === null) && !parentAttrNames.includes("endIcon")) {
|
||||
suggestions.push({
|
||||
messageId: "useEndIcon",
|
||||
fix: makeFix("endIcon"),
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
return {
|
||||
Element(node) {
|
||||
if (node.name !== "button" && node.name !== "a") {
|
||||
@@ -37,9 +188,11 @@ export default {
|
||||
|
||||
// <bit-icon> child
|
||||
if (child.name === "bit-icon") {
|
||||
const suggest = createSuggestions(node, child);
|
||||
context.report({
|
||||
node: child,
|
||||
message: errorMessage,
|
||||
messageId: "noIconChildren",
|
||||
...(suggest.length ? { suggest } : {}),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -59,9 +212,11 @@ export default {
|
||||
}
|
||||
|
||||
if (/\bbwi\b/.test(classValue)) {
|
||||
const suggest = createSuggestions(node, child);
|
||||
context.report({
|
||||
node: child,
|
||||
message: errorMessage,
|
||||
messageId: "noIconChildren",
|
||||
...(suggest.length ? { suggest } : {}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import rule, { errorMessage } from "./no-icon-children-in-bit-button.mjs";
|
||||
import rule from "./no-icon-children-in-bit-button.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
@@ -49,49 +49,192 @@ ruleTester.run("no-icon-children-in-bit-button", rule.default, {
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: "should warn on <i> with bwi class inside button[bitButton]",
|
||||
name: "should suggest startIcon for <i> with bwi class inside button[bitButton]",
|
||||
code: `<button bitButton buttonType="primary"><i class="bwi bwi-plus"></i> Add</button>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton buttonType="primary" startIcon="bwi-plus">Add</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <i> with bwi class and extra classes inside button[bitButton]",
|
||||
name: "should suggest fix when <i> has droppable spacing classes",
|
||||
code: `<button bitButton><i class="bwi bwi-lock tw-me-2" aria-hidden="true"></i> Lock</button>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton startIcon="bwi-lock">Lock</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <i> with bwi class inside a[bitButton]",
|
||||
name: "should not suggest fix when <i> has non-droppable classes",
|
||||
code: `<button bitButton><i class="bwi bwi-lock tw-text-red" aria-hidden="true"></i> Lock</button>`,
|
||||
errors: [{ messageId: "noIconChildren" }],
|
||||
},
|
||||
{
|
||||
name: "should suggest startIcon for <i> with bwi class inside a[bitButton]",
|
||||
code: `<a bitButton buttonType="secondary"><i class="bwi bwi-external-link"></i> Link</a>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<a bitButton buttonType="secondary" startIcon="bwi-external-link">Link</a>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <bit-icon> inside button[bitButton]",
|
||||
name: "should suggest startIcon for <bit-icon> inside button[bitButton]",
|
||||
code: `<button bitButton buttonType="primary"><bit-icon name="bwi-lock"></bit-icon> Lock</button>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton buttonType="primary" startIcon="bwi-lock">Lock</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <bit-icon> inside a[bitButton]",
|
||||
name: "should suggest startIcon for <bit-icon> inside a[bitButton]",
|
||||
code: `<a bitButton><bit-icon name="bwi-clone"></bit-icon> Copy</a>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<a bitButton startIcon="bwi-clone">Copy</a>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on multiple icon children inside bitButton",
|
||||
name: "should suggest startIcon and endIcon for multiple icon children",
|
||||
code: `<button bitButton><i class="bwi bwi-plus"></i> Add <i class="bwi bwi-angle-down"></i></button>`,
|
||||
errors: [{ message: errorMessage }, { message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton startIcon="bwi-plus">Add <i class="bwi bwi-angle-down"></i></button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useEndIcon",
|
||||
output: `<button bitButton endIcon="bwi-angle-down"><i class="bwi bwi-plus"></i> Add</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on both <i> and <bit-icon> children",
|
||||
name: "should suggest for both <i> and <bit-icon> children",
|
||||
code: `<button bitButton><i class="bwi bwi-plus"></i><bit-icon name="bwi-lock"></bit-icon></button>`,
|
||||
errors: [{ message: errorMessage }, { message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton startIcon="bwi-plus"><bit-icon name="bwi-lock"></bit-icon></button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useEndIcon",
|
||||
output: `<button bitButton endIcon="bwi-lock"><i class="bwi bwi-plus"></i></button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <i> with bwi class inside a[bitLink]",
|
||||
name: "should suggest startIcon for <i> with bwi class inside a[bitLink]",
|
||||
code: `<a bitLink><i class="bwi bwi-external-link"></i> Link</a>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<a bitLink startIcon="bwi-external-link">Link</a>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should warn on <bit-icon> inside button[bitLink]",
|
||||
name: "should suggest startIcon for <bit-icon> inside button[bitLink]",
|
||||
code: `<button bitLink><bit-icon name="bwi-lock"></bit-icon> Lock</button>`,
|
||||
errors: [{ message: errorMessage }],
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitLink startIcon="bwi-lock">Lock</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should suggest endIcon when icon is after text",
|
||||
code: `<button bitButton>Save <i class="bwi bwi-angle-down"></i></button>`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useEndIcon",
|
||||
output: `<button bitButton endIcon="bwi-angle-down">Save</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "should suggest startIcon for <i> with aria-hidden (no extra classes)",
|
||||
code: `<button bitButton><i class="bwi bwi-plus" aria-hidden="true"></i> Add</button>`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "noIconChildren",
|
||||
suggestions: [
|
||||
{
|
||||
messageId: "useStartIcon",
|
||||
output: `<button bitButton startIcon="bwi-plus">Add</button>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user