diff --git a/eslint.config.mjs b/eslint.config.mjs index 974aaafeef6..2e35b011c73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -208,6 +208,7 @@ export default tseslint.config( { ignoreIfHas: ["bitPasswordInputToggle"] }, ], "@bitwarden/components/no-bwi-class-usage": "warn", + "@bitwarden/components/no-icon-children-in-bit-button": "warn", }, }, diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 4954effb6c0..51ec62f8787 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -1,4 +1,12 @@ -import { Meta, Story, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks"; +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Title, + Description, +} from "@storybook/addon-docs/blocks"; import * as stories from "./link.stories"; @@ -33,15 +41,25 @@ You can use one of the following variants by providing it as the `linkType` inpu If you want to display a link with a smaller text size, apply the `tw-text-sm` class. This will match the `body2` variant of the Typography directive. -## With icons +## With Icon -Text Links/buttons can have icons on left or the right. +Use the `startIcon` and `endIcon` inputs to add a Bitwarden icon (`bwi-*`) before or after the link +label. Do not use a `` component inside the link as this may not have the correct styling +and spacing. -To indicate a new or add action, the icon on is used on the -left. +### Icon before the label -An angle icon, , is used on the left to indicate an expand to -show/hide additional content. +```html +Add item +``` + +### Icon after the label + +```html +Next +``` + + ## Accessibility diff --git a/libs/eslint/components/index.mjs b/libs/eslint/components/index.mjs index 101fdde414c..23116dc6958 100644 --- a/libs/eslint/components/index.mjs +++ b/libs/eslint/components/index.mjs @@ -1,11 +1,13 @@ import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs"; import requireThemeColorsInSvg from "./require-theme-colors-in-svg.mjs"; import noBwiClassUsage from "./no-bwi-class-usage.mjs"; +import noIconChildrenInBitButton from "./no-icon-children-in-bit-button.mjs"; export default { rules: { "require-label-on-biticonbutton": requireLabelOnBiticonbutton, "require-theme-colors-in-svg": requireThemeColorsInSvg, "no-bwi-class-usage": noBwiClassUsage, + "no-icon-children-in-bit-button": noIconChildrenInBitButton, }, }; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.mjs b/libs/eslint/components/no-icon-children-in-bit-button.mjs new file mode 100644 index 00000000000..926093f2e44 --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.mjs @@ -0,0 +1,74 @@ +export const errorMessage = + 'Avoid placing icon elements ( or ) inside a bitButton or bitLink. ' + + "Use the [startIcon] or [endIcon] inputs instead. " + + 'Example: '; + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Discourage using icon child elements inside bitButton; use startIcon/endIcon inputs instead", + category: "Best Practices", + recommended: true, + }, + schema: [], + }, + create(context) { + return { + Element(node) { + if (node.name !== "button" && node.name !== "a") { + return; + } + + const allAttrNames = [ + ...(node.attributes?.map((attr) => attr.name) ?? []), + ...(node.inputs?.map((input) => input.name) ?? []), + ]; + + if (!allAttrNames.includes("bitButton") && !allAttrNames.includes("bitLink")) { + return; + } + + for (const child of node.children ?? []) { + if (!child.name) { + continue; + } + + // child + if (child.name === "bit-icon") { + context.report({ + node: child, + message: errorMessage, + }); + continue; + } + + // child with bwi class + if (child.name === "i") { + const classAttrs = [ + ...(child.attributes?.filter((attr) => attr.name === "class") ?? []), + ...(child.inputs?.filter((input) => input.name === "class") ?? []), + ]; + + for (const classAttr of classAttrs) { + const classValue = classAttr.value || ""; + + if (typeof classValue !== "string") { + continue; + } + + if (/\bbwi\b/.test(classValue)) { + context.report({ + node: child, + message: errorMessage, + }); + break; + } + } + } + } + }, + }; + }, +}; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs new file mode 100644 index 00000000000..656a320678d --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs @@ -0,0 +1,97 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import rule, { errorMessage } from "./no-icon-children-in-bit-button.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require("@angular-eslint/template-parser"), + }, +}); + +ruleTester.run("no-icon-children-in-bit-button", rule.default, { + valid: [ + { + name: "should allow bitButton with startIcon input", + code: ``, + }, + { + name: "should allow bitButton with endIcon input", + code: ``, + }, + { + name: "should allow a[bitButton] with startIcon input", + code: `Link`, + }, + { + name: "should allow with bwi inside a regular button (no bitButton)", + code: ``, + }, + { + name: "should allow inside a regular div", + code: `
`, + }, + { + name: "should allow bitButton with only text content", + code: ``, + }, + { + name: "should allow without bwi class inside bitButton", + code: ``, + }, + { + name: "should allow bitLink with startIcon input", + code: `Link`, + }, + { + name: "should allow bitLink with only text content", + code: `Link`, + }, + ], + invalid: [ + { + name: "should warn on with bwi class inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class and extra classes inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitButton]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside a[bitButton]", + code: ` Copy`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on multiple icon children inside bitButton", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on both and children", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitLink]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitLink]", + code: ``, + errors: [{ message: errorMessage }], + }, + ], +});