1
0
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:
Oscar Hinton
2026-02-26 17:17:23 +01:00
committed by GitHub
parent abbfda124f
commit 5c5102fa30
8 changed files with 599 additions and 58 deletions

View File

@@ -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),
);
}

View File

@@ -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

View File

@@ -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>`,

View 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;
}

View File

@@ -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;

View File

@@ -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 }],
},
],

View File

@@ -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;
}

View File

@@ -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>`,
},
],
},
],
},
],
});