Repository: Sage/carbon (ts)

Issue:
Badge component should support static informative display
### Desired behaviour

Quite often the `Badge` component is used for plain informative display purposes and is not intended to be interacted with. To achieve that, the `Badge` component (by default?) should support only the display of the counter, not capture focus, and don't show the clear button.

### Current behaviour

However, it still shows the "X" button on hover and the badge is capturing the focus and does not support any option to turn off that behavior.

https://user-images.githubusercontent.com/30114244/208888908-cbcd0f92-b31f-4323-bab1-4c71c6ad2480.mov


### Suggested Solution

The simplest solution would be to turn off the "Clear button" display and interactivity in case the (optional) `onClick` handler is not present:

<img width="1021" alt="Zrzut ekranu 2022-12-21 o 11 57 51" src="https://user-images.githubusercontent.com/30114244/208889376-b72ae88a-d40e-4d08-9663-82f53b42d28b.png">


### CodeSandbox or Storybook URL

_No response_

### Anything else we should know?

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
feat(badge): allow component to be display only - FE-5687
This change allows Badge to be display only by not passing an `onClick` prop to the component. In this variant, the badge will render as a `span` element instead of a `button`.

fixes #5706

### Proposed behaviour

Have a display only variant of `Badge` that is not interactive but looks the same as `Badge` currently does.

### Current behaviour

`Badge` component cannot be display only. Component is always interactive, even without an `onClick` function a hover style is applied implying that the component is able to be interacted with. 

### Checklist

<!-- Each PR should include the following -->

- [x] Commits follow our style guide
- [x] Related issues linked in commit messages if required
- [ ] Screenshots are included in the PR if useful
- [x] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Cypress automation tests added or updated if required
- [x] Storybook added or updated if required
- [ ] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [x] Typescript `d.ts` file added or updated if required
- [x] Related docs have been updated if required

#### QA

- [ ] Tested in CodeSandbox/storybook
- [ ] Add new Cypress test coverage if required
- [ ] Carbon implementation matches Design System/designs
- [ ] UI Tests GitHub check reviewed if required

### Additional context

N/A

### Testing instructions

A new example has been created called `display only`. This can be seen in Storybook. 
A codesanbox can be created if needs be too.

Patch:
diff --git a/src/components/badge/badge.component.tsx b/src/components/badge/badge.component.tsx
index 0fc0b786e..fbbd5b884 100644
--- a/src/components/badge/badge.component.tsx
+++ b/src/components/badge/badge.component.tsx
@@ -1,12 +1,14 @@
 import React from "react";
 import {
   StyledBadgeWrapper,
-  StyledButton,
   StyledCrossIcon,
   StyledCounter,
+  StyledBadge,
 } from "./badge.style";
 
 export interface BadgeProps {
+  /** Prop to specify an aria-label for the component */
+  "aria-label"?: string;
   /** The badge will be added to this element */
   children: React.ReactNode;
   /** The number rendered in the badge component */
@@ -15,21 +17,44 @@ export interface BadgeProps {
   onClick?: (ev: React.MouseEvent<HTMLElement>) => void;
 }
 
-export const Badge = ({ children, counter = 0, onClick }: BadgeProps) => {
-  return (
-    <StyledBadgeWrapper>
-      {counter > 0 && (
-        <StyledButton
-          data-component="badge"
-          buttonType="secondary"
-          onClick={onClick}
-        >
-          <StyledCrossIcon data-element="badge-cross-icon" type="cross" />
+export const Badge = ({
+  "aria-label": ariaLabel,
+  children,
+  counter = 0,
+  onClick,
+}: BadgeProps) => {
+  const shouldDisplayCounter = counter > 0;
+  const counterToDisplay = counter > 99 ? 99 : counter;
+
+  const renderCorrectBadge = () => {
+    const props = onClick
+      ? {
+          buttonType: "secondary",
+          onClick,
+        }
+      : {
+          "aria-label": ariaLabel,
+        };
+
+    if (shouldDisplayCounter) {
+      return (
+        <StyledBadge data-component="badge" {...props}>
+          {onClick && (
+            <StyledCrossIcon data-element="badge-cross-icon" type="cross" />
+          )}
           <StyledCounter data-element="badge-counter">
-            {counter > 99 ? 99 : counter}
+            {counterToDisplay}
           </StyledCounter>
-        </StyledButton>
-      )}
+        </StyledBadge>
+      );
+    }
+
+    return null;
+  };
+
+  return (
+    <StyledBadgeWrapper>
+      {renderCorrectBadge()}
       {children}
     </StyledBadgeWrapper>
   );
diff --git a/src/components/badge/badge.spec.tsx b/src/components/badge/badge.spec.tsx
index 8fba9cb5e..1b833271e 100644
--- a/src/components/badge/badge.spec.tsx
+++ b/src/components/badge/badge.spec.tsx
@@ -1,9 +1,10 @@
 import React from "react";
 import { mount } from "enzyme";
 import { ThemeProvider } from "styled-components";
+import { assertStyleMatch } from "__spec_helper__/test-utils";
 import Badge from "./badge.component";
 import Button from "../button";
-import { StyledCounter } from "./badge.style";
+import { StyledCounter, StyledBadge } from "./badge.style";
 import { baseTheme } from "../../style/themes";
 
 const renderComponent = (props = {}) => (
@@ -17,12 +18,18 @@ const renderComponent = (props = {}) => (
 const BADGE = '[data-component="badge"]';
 
 describe("Badge", () => {
-  it("should render correctly", () => {
+  it("should render correctly if counter is a number", () => {
     const wrapper = mount(renderComponent({ counter: 1 }));
 
     expect(wrapper.find(BADGE).exists()).toBe(true);
   });
 
+  it("should render correctly if counter is a string", () => {
+    const wrapper = mount(renderComponent({ counter: "1" }));
+
+    expect(wrapper.find(BADGE).exists()).toBe(true);
+  });
+
   describe("when counter is between 1 and 99", () => {
     const wrapper = mount(renderComponent({ counter: 50 }));
 
@@ -54,4 +61,45 @@ describe("Badge", () => {
       expect(wrapper.find(BADGE).exists()).toBe(false);
     });
   });
+
+  describe("Renders correct elements", () => {
+    let wrapper;
+
+    it("should render as a button element when onClick is present", () => {
+      wrapper = mount(renderComponent({ counter: 9, onClick: () => {} }));
+      expect(wrapper.find(StyledBadge).exists()).toBe(true);
+      expect(wrapper.find("button").exists()).toBe(true);
+    });
+
+    it("should render as a span element when onClick is not present", () => {
+      wrapper = mount(renderComponent({ counter: 9 }));
+      expect(wrapper.find(StyledBadge).exists()).toBe(true);
+      expect(wrapper.find("span").exists()).toBe(true);
+    });
+  });
+
+  describe("Accessibility", () => {
+    const ariaLabelText = "Generic aria message";
+    const wrapper = mount(
+      renderComponent({ counter: 9, "aria-label": ariaLabelText })
+    );
+
+    it("should have the relevant aria-label when aria-label is specified", () => {
+      expect(wrapper.find(Badge).first().prop("aria-label")).toBe(
+        ariaLabelText
+      );
+    });
+  });
+
+  describe("Styles", () => {
+    it("should apply the correct cursor style when onClick is not specified", () => {
+      const wrapper = mount(renderComponent({ counter: 9 }));
+      assertStyleMatch(
+        {
+          cursor: `default`,
+        },
+        wrapper.find(StyledBadge)
+      );
+    });
+  });
 });
diff --git a/src/components/badge/badge.stories.mdx b/src/components/badge/badge.stories.mdx
index 47ba26665..d2380de97 100644
--- a/src/components/badge/badge.stories.mdx
+++ b/src/components/badge/badge.stories.mdx
@@ -1,6 +1,7 @@
 import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
 
 import Badge from ".";
+import Box from "../box";
 import Button from "../button";
 
 <Meta title="Badge" parameters={{ info: { disable: true } }} />
@@ -37,13 +38,13 @@ import Badge from "carbon-react/lib/components/badge";
 
 <Canvas>
   <Story name="default">
-    <div style={{ margin: "40px" }}>
-      <Badge counter={9}>
+    <Box margin="40px">
+      <Badge counter={9} onClick={()=>{}}>
         <Button style={{ marginRight: 0 }} buttonType="tertiary">
           Filter
         </Button>
       </Badge>
-    </div>
+    </Box>
   </Story>
 </Canvas>
 
@@ -53,13 +54,13 @@ Badge doesn't support more than 2 digits
 
 <Canvas>
   <Story name="with 3 digits">
-    <div style={{ margin: "40px" }}>
-      <Badge counter={130}>
+    <Box margin="40px">
+      <Badge counter={130} onClick={()=>{}}>
         <Button style={{ marginRight: 0 }} buttonType="tertiary">
           Filter
         </Button>
       </Badge>
-    </div>
+    </Box>
   </Story>
 </Canvas>
 
@@ -69,11 +70,28 @@ If the counter is less than 1 badge will not show itself
 
 <Canvas>
   <Story name="with counter 0">
-    <div style={{ margin: "40px" }}>
-      <Badge counter={0}>
+    <Box margin="40px">
+      <Badge counter={0} onClick={()=>{}}>
         <Button buttonType="tertiary">Filter</Button>
       </Badge>
-    </div>
+    </Box>
+  </Story>
+</Canvas>
+
+### Display Only
+
+To acheive a `display only` version of this component, you can simply not pass an `onClick` function to the component. This means that instead of rendering as a `button` element, 
+the component will render as a `span`.
+
+<Canvas>
+  <Story name="display only">
+    <Box margin="40px">
+      <Badge counter={9} aria-label="The counter is currently displaying 9">
+        <Button style={{ marginRight: 0 }} buttonType="tertiary">
+          Filter
+        </Button>
+      </Badge>
+    </Box>
   </Story>
 </Canvas>
 
diff --git a/src/components/badge/badge.style.ts b/src/components/badge/badge.style.ts
index 31dafcc4c..1e1d7e497 100644
--- a/src/components/badge/badge.style.ts
+++ b/src/components/badge/badge.style.ts
@@ -1,8 +1,19 @@
-import styled from "styled-components";
+import styled, { css } from "styled-components";
 import StyledIcon from "../icon/icon.style";
 import Button from "../button";
 import Icon from "../icon";
 
+const commonStyles = `
+  overflow: hidden;
+  border-radius: 50%;
+  position: absolute;
+  top: -11px;
+  right: -11px;
+  padding: 0;
+  margin-right: 0;
+  background: var(--colorsActionMajorYang100);
+`;
+
 const StyledBadgeWrapper = styled.div`
   position: relative;
   display: inline-block;
@@ -14,43 +25,58 @@ const StyledCounter = styled.div`
   margin-top: -1px;
 `;
 
-const StyledButton = styled(Button)`
-  padding: 0;
-  width: 22px;
-  min-height: 22px;
-  border-radius: 50%;
-  overflow: hidden;
-  text-align: center;
-  position: absolute;
-  top: -11px;
-  right: -11px;
-  margin-right: 0;
-  background: var(--colorsActionMajorYang100);
+const StyledBadge = styled.span.attrs(({ onClick }) => ({
+  as: onClick ? Button : undefined,
+}))`
+  ${commonStyles}
+  cursor: default;
+  align-items: center;
+  display: inline-flex;
+  justify-content: center;
+  width: 18px;
+  min-height: 18px;
+  border: solid 2px transparent;
+  border-color: var(--colorsActionMajor500);
+  color: var(--colorsActionMajor500);
 
   ::-moz-focus-inner {
     border: none;
   }
 
-  &:hover,
-  &:focus {
-    background: var(--colorsActionMajor500);
-    border: none;
-    ${StyledCounter} {
-      display: none;
-    }
-
-    ${StyledIcon} {
-      display: block;
-      width: auto;
-      height: auto;
-      margin-right: 0;
-
-      :before {
-        font-size: 16px;
-        color: var(--colorsActionMajorYang100);
+  ${({ onClick }) => css`
+    ${onClick &&
+    `
+      ${commonStyles}
+      width: 22px;
+      min-height: 22px;
+      text-align: center;
+
+      ::-moz-focus-inner {
+        border: none;
       }
-    }
-  }
+
+      &:hover,
+      &:focus {
+        background: var(--colorsActionMajor500);
+        border: none;
+        ${StyledCounter} {
+          display: none;
+        }
+
+        ${StyledIcon} {
+          display: block;
+          width: auto;
+          height: auto;
+          margin-right: 0;
+
+          :before {
+            font-size: 16px;
+            color: var(--colorsActionMajorYang100);
+          }
+        }
+      }
+    `}
+  `}
 `;
 
 const StyledCrossIcon = styled(Icon)`
@@ -58,4 +84,4 @@ const StyledCrossIcon = styled(Icon)`
   display: none;
 `;
 
-export { StyledBadgeWrapper, StyledButton, StyledCrossIcon, StyledCounter };
+export { StyledBadge, StyledBadgeWrapper, StyledCrossIcon, StyledCounter };


Test patch:
diff --git a/src/components/badge/badge-test.stories.tsx b/src/components/badge/badge-test.stories.tsx
index ada9ba778..2c5fddfa4 100644
--- a/src/components/badge/badge-test.stories.tsx
+++ b/src/components/badge/badge-test.stories.tsx
@@ -2,6 +2,7 @@ import React from "react";
 import { action } from "@storybook/addon-actions";
 
 import Badge from "./badge.component";
+import Box from "../box";
 import Button from "../button";
 
 export default {
@@ -33,9 +34,20 @@ export const DefaultStory = ({ counter, ...args }: BadgeStoryProps) => {
   );
 };
 
-DefaultStory.story = {
-  name: "default",
-  args: {
-    counter: 1,
-  },
+export const DisplayOnlyStory = ({ counter, ...args }: BadgeStoryProps) => {
+  return (
+    <Box margin="40px">
+      <Badge counter={counter} {...args}>
+        <Button mr={0} buttonType="tertiary">
+          Filter
+        </Button>
+      </Badge>
+    </Box>
+  );
 };
+
+DefaultStory.storyName = "default";
+DefaultStory.args = { counter: 1 };
+
+DisplayOnlyStory.storyName = "display only";
+DisplayOnlyStory.args = { counter: 1 };
diff --git a/src/components/badge/badge.test.js b/src/components/badge/badge.test.js
index 1d9d3fbd8..3f3195ade 100644
--- a/src/components/badge/badge.test.js
+++ b/src/components/badge/badge.test.js
@@ -51,15 +51,17 @@ context("Testing Badge component", () => {
 
     it.each([[0], [-12], ["test"], [CHARACTERS.SPECIALCHARACTERS]])(
       "should check Badge counter is not visible when using %s param",
-      (incorectValue) => {
-        CypressMountWithProviders(<BadgeComponent counter={incorectValue} />);
+      (incorrectValue) => {
+        CypressMountWithProviders(<BadgeComponent counter={incorrectValue} />);
 
         badge().should("not.exist");
       }
     );
 
     it("badge should display cross icon when hovered over", () => {
-      CypressMountWithProviders(<BadgeComponent counter="99" />);
+      CypressMountWithProviders(
+        <BadgeComponent onClick={() => {}} counter="99" />
+      );
 
       badge()
         .realHover()
@@ -70,6 +72,18 @@ context("Testing Badge component", () => {
       badgeCrossIcon().should("be.visible");
     });
 
+    it("badge should not display cross icon when hovered over with no onClick function passed to component", () => {
+      CypressMountWithProviders(<BadgeComponent counter="99" />);
+
+      badge()
+        .realHover()
+        .should("have.css", "background")
+        .then(($el) => {
+          expect($el).contains("rgb(255, 255, 255)");
+        });
+      badgeCrossIcon().should("not.exist");
+    });
+
     it("should call onClick callback when a click event is triggered", () => {
       const callback = cy.stub();
 
@@ -84,12 +98,20 @@ context("Testing Badge component", () => {
           expect(callback).to.have.been.calledOnce;
         });
     });
+
+    it("should check ariaLabel for Badge component", () => {
+      CypressMountWithProviders(
+        <BadgeComponent counter={9} aria-label="cypress-aria" />
+      );
+      badge().should("exist");
+      badge().should("have.attr", "aria-label", "cypress-aria");
+    });
   });
 
   describe("Accessibility tests for Badge component", () => {
     // FE-5596
-    it.skip("should pass accessibilty tests for Badge default story", () => {
-      CypressMountWithProviders(<BadgeComponent counter />);
+    it.skip("should pass accessibility tests for Badge default story", () => {
+      CypressMountWithProviders(<BadgeComponent counter={9} />);
       cy.checkAccessibility();
     });