From 0ffdb21c0050bbef1cf73c509170917f987b15ab Mon Sep 17 00:00:00 2001 From: NriotHrreion Date: Fri, 18 Sep 2026 10:16:02 +0800 Subject: [PATCH] fix(badge): keep icon-only badges centered (#127) Apply optical alignment only when the badge has content --- packages/ui/src/base/badge/badge.stories.tsx | 38 ++++++++++++++++++++ packages/ui/src/base/badge/badge.test.tsx | 20 ++++++++++- packages/ui/src/base/badge/badge.tsx | 4 +-- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/base/badge/badge.stories.tsx b/packages/ui/src/base/badge/badge.stories.tsx index 4a01bb1..934f4cb 100644 --- a/packages/ui/src/base/badge/badge.stories.tsx +++ b/packages/ui/src/base/badge/badge.stories.tsx @@ -209,6 +209,44 @@ export const IconOnly: Story = { }, }; +export const IconOpticallyAlignedIconOnly: Story = { + argTypes: { + children: { + control: false, + }, + icon: { + control: false, + }, + }, + args: { + children: undefined, + }, + render: (args) => ( +
+ + + + + +
+ ), + play: async ({ canvas }) => { + for(const [icon, label] of [ + ["issue-open-m", "Open"], + ["issue-close", "Closed"], + ["status_failed", "Failed"], + ["status_success", "Passed"], + ["status_pending", "Pending"], + ]) { + const badge = canvas.getByRole("img", { name: label }); + + await expect(canvas.getByTestId(`${icon}-icon`)).not.toHaveClass("-gl-ml-2"); + await expect(badge).toHaveClass("!gl-px-2"); + await expect(badge.querySelector(".gl-badge-content")).toBeNull(); + } + }, +}; + export const Truncated: Story = { argTypes: { children: { diff --git a/packages/ui/src/base/badge/badge.test.tsx b/packages/ui/src/base/badge/badge.test.tsx index e8ff243..0e5a9ed 100644 --- a/packages/ui/src/base/badge/badge.test.tsx +++ b/packages/ui/src/base/badge/badge.test.tsx @@ -73,6 +73,7 @@ describe("GlBadge", () => { it.each([ [{ icon: "issue-open-m" }, true], [{ icon: "issue-close" }, true], + [{ icon: "status_failed", iconOpticallyAligned: true }, true], [{ icon: "license", iconOpticallyAligned: true }, true], [{ icon: "license" }, false], ] as const)("aligns circular icons for %s", (props, expected) => { @@ -81,6 +82,22 @@ describe("GlBadge", () => { expect(markup.includes("-gl-ml-2")).toBe(expected); }); + it.each([ + ["issue-open-m", false], + ["issue-close", false], + ["status_failed", true], + ] as const)("keeps the icon-only %s badge centered", (icon, iconOpticallyAligned) => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).not.toContain("-gl-ml-2"); + expect(markup).toContain("role=\"img\""); + expect(markup).toContain("aria-label=\"Status\""); + expect(markup).toContain("!gl-px-2"); + expect(markup).not.toContain("gl-badge-content"); + }); + it("warns when an icon-only badge has no aria-label", () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); @@ -132,11 +149,12 @@ describe("GlBadge", () => { it("keeps the img role for icon-only link badges", () => { const markup = renderBadge( - { "aria-label": "Scheduled", href: "https://www.gitlab.com", icon: "calendar" }, + { "aria-label": "Closed", href: "https://www.gitlab.com", icon: "issue-close" }, null, ); expect(markup).toContain("role=\"img\""); + expect(markup).not.toContain("-gl-ml-2"); }); }); }); diff --git a/packages/ui/src/base/badge/badge.tsx b/packages/ui/src/base/badge/badge.tsx index f4e036b..5c39907 100644 --- a/packages/ui/src/base/badge/badge.tsx +++ b/packages/ui/src/base/badge/badge.tsx @@ -45,7 +45,7 @@ export type GlBadgeProps = BaseBadgeProps & { href?: string; /** Name of an icon from `@gitlab/svgs` shown before the text. */ icon?: string; - /** Optically aligns circular icons with the badge. */ + /** Optically aligns circular icons when the badge also has content. */ iconOpticallyAligned?: boolean; /** Icon size: `sm` (12px) or `md` (16px). */ iconSize?: GlBadgeIconSize; @@ -147,7 +147,7 @@ const GlBadge = forwardRef(function GlBadge({ {icon ? (