Repository: Sage/carbon (ts)

Issue:
hidden prop doesn't exist in Image component
### Current behaviour

hidden prop doesn't exist in Image component.

Error message : Cannot assign type '{ size: string; src: string; hidden: true; }' to type 'IntrinsicAttributes & StyledImageProps'.
  Property 'hidden' does not exist on type 'IntrinsicAttributes & StyledImageProps

No problem on the actual version we are using 110.3.0

### Expected behaviour

hidden prop should exists in Image component.



### CodeSandbox or Storybook URL

Not reproduced in the CodeSandbox

### JIRA Ticket (Sage Only)

_No response_

### Suggested Solution

_No response_

### Carbon Version

110.10.2

### Design Tokens Version

_No response_

### What browsers are you seeing the problem on?

Chrome

### What Operating System are you seeing the problem on?

MacOS

### Anything else we should know?

_No response_

### Confidentiality

- [X] I confirm there is no confidential or commercially sensitive information included.

Pull request:
feat(image): add hidden prop
closes #5501

### Proposed behaviour

<!--
A clear and concise description of what changes this PR makes.

If applicable, add screenshots of a codesandbox to help explain your request. You can paste these directly into GitHub.

Please DO NOT share screenshots or the source code of your project.

You can create a codesandbox to show the behaviour before/after this pull request by forking this template https://codesandbox.io/s/carbon-quickstart-j5pb2

If you include a CodeSandbox link, the bot will fork it with the new built version of carbon.
If you have a commit that includes fixes #XXXX and issue #XXXX has a CodeSandbox link in the body, the bot will fork
it with the new built version of carbon.
-->

- Add `hidden` prop to `Image` component - which denotes the image element in the DOM should not be rendered by the browser, nor accessible via keyboard or a screen reader.
- Introduce new story to demonstrate `hidden` prop - to validate if a hidden `Image` is not recognised by a screen reader.
- Refactor stories to CSF with MDX docs setup

### Current behaviour

<!--
A clear and concise description of the behaviour before this change.

If applicable, add screenshots. You can paste these directly into GitHub.
-->
- Even though technically the `hidden` prop can be passed to `Image`, as its not an official documented prop it results in a typescript error.  

### Checklist

<!-- Each PR should include the following -->

- [ ] Commits follow our style guide
- [ ] Related issues linked in commit messages if required
- [ ] Screenshots are included in the PR if useful
- [ ] All themes are supported if required
- [ ] Unit tests added or updated if required
- [ ] Cypress automation tests added or updated if required
- [ ] Storybook added or updated if required
- [ ] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [ ] Typescript `d.ts` file added or 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

### Testing instructions

- In storybook:
    - [ ] In the `With hidden prop` story - double check that the image is not recognisable by Mac VoiceOver when hidden
    - [ ] Verify all `Image` stories have their source code correctly rendered under `Show code` and is inline with master.

Patch:
diff --git a/src/components/image/image.spec.tsx b/src/components/image/image.spec.tsx
index bd6ccd914..be7fc4189 100644
--- a/src/components/image/image.spec.tsx
+++ b/src/components/image/image.spec.tsx
@@ -28,6 +28,12 @@ describe("Image", () => {
     expect(wrapper.find("div").exists()).toBeTruthy();
   });
 
+  it("when hidden prop is passed, renders Image with `hidden` html attribute", () => {
+    const wrapper = mount(<Image src="foo.jpg" alt="foo" hidden />);
+
+    expect(wrapper.find("img[hidden]").exists()).toBeTruthy();
+  });
+
   describe("prop types", () => {
     const mockGlobal = jest
       .spyOn(global.console, "error")
diff --git a/src/components/image/image.stories.mdx b/src/components/image/image.stories.mdx
index 2ec60296d..f9e7ac365 100644
--- a/src/components/image/image.stories.mdx
+++ b/src/components/image/image.stories.mdx
@@ -1,12 +1,14 @@
 import { Meta, Story, Canvas } from "@storybook/addon-docs";
-import { ArgsTable } from "@storybook/components";
+import StyledSystemProps from "../../../.storybook/utils/styled-system-props";
 
 import Image from "./image.component";
+
 import Box from "../box";
 import Typography from "../typography";
-import StyledSystemProps from "../../../.storybook/utils/styled-system-props";
 import useMediaQuery from "../../hooks/useMediaQuery";
 
+import * as stories from "./image.stories.tsx";
+
 <Meta title="Image" />
 
 # Image
@@ -36,24 +38,7 @@ By default the component will render as a `div` element and use the `backgroundI
 css attribute.
 
 <Canvas>
-  <Story name="default" parameters={{ info: { disable: true } }}>
-    <Image
-      m={3}
-      height="700px"
-      backgroundImage={`url(${require("../../../.assets/flexible.svg")})`}
-    >
-      <Box
-        height="700px"
-        display="flex"
-        alignItems="center"
-        justifyContent="center"
-      >
-        <Typography variant="h4">
-          Here is an example of some overlayed text
-        </Typography>
-      </Box>
-    </Image>
-  </Story>
+  <Story name="default" story={stories.DefaultStory} />
 </Canvas>
 
 ### As an img element
@@ -62,27 +47,15 @@ If you want to render an `img` element pass in a value to the `src` prop. The co
 rendered in this way.
 
 <Canvas>
-  <Story name="as an img" parameters={{ info: { disable: true } }}>
-    <Image
-      m={3}
-      ml={8}
-      alt="Example alt text"
-      src={require("../../../.assets/point.svg")}
-    />
-    <Image
-      m={3}
-      ml={5}
-      size="200px"
-      alt="Example alt text"
-      src={require("../../../.assets/brush.svg")}
-    />
-    <Image
-      m={3}
-      size="300px"
-      alt="Example alt text"
-      src={require("../../../.assets/collaborate.svg")}
-    />
-  </Story>
+  <Story name="as an img" story={stories.AsAnImg} />
+</Canvas>
+
+## With `hidden` prop
+
+To hide the component completely - meaning hidden visually and from being accessible via keyboard navigation or a screen reader - the `hidden` prop can be passed. The underlying element remains in the DOM but is semantically hidden.
+
+<Canvas>
+  <Story name="with hidden prop" story={stories.WithHiddenProp} />
 </Canvas>
 
 ### Custom responsive behaviour
@@ -93,80 +66,12 @@ viewed in the Canvas tab using full-screen mode with device or viewport emulatio
 <Canvas>
   <Story
     name="custom responsive behaviour"
-    parameters={{ chromatic: { disable: true }, info: { disable: true } }}
-  >
-    {() => {
-      const query1 = useMediaQuery("(max-width: 1000px)");
-      const query2 = useMediaQuery("(max-width: 900px)");
-      const query3 = useMediaQuery("(max-width: 800px)");
-      const responsiveProps = () => {
-        if (query3) {
-          return {
-            backgroundSize: "contain",
-            backgroundImage: `url(${require("../../../.assets/collaborate.svg")})`,
-            m: 1,
-          };
-        }
-        if (query2) {
-          return {
-            backgroundSize: "contiain",
-            backgroundRepeat: "repeat",
-            backgroundImage: `url(${require("../../../.assets/point.svg")})`,
-            m: 2,
-          };
-        }
-        if (query1) {
-          return {
-            backgroundImage: `url(${require("../../../.assets/collaborate.svg")})`,
-            m: 3,
-          };
-        }
-        return {
-          backgroundImage: `url(${require("../../../.assets/point.svg")})`,
-          m: 4,
-        };
-      };
-      return (
-        <Image {...responsiveProps()}>
-          <Box
-            height="700px"
-            display="flex"
-            alignItems="center"
-            justifyContent="center"
-          >
-            <Typography variant="h4">
-              Here is an example of some overlayed text
-            </Typography>
-          </Box>
-        </Image>
-      );
-    }}
-  </Story>
+    story={stories.CustomResponsiveBehaviour}
+  />
 </Canvas>
 
 ## Props
 
 ### Image
 
-<ArgsTable
-  rows={[
-    {
-      name: "src",
-      type: { summary: "string" },
-      description:
-        "Any valid file path for an image, passing a value to this prop will render component as `<img />`",
-      required: false,
-      defaultValue: "-",
-    },
-    {
-      name: "alt",
-      type: { summary: "string" },
-      description:
-        "Text to display when component renders as `<img />` but fails to load the `src`",
-      required: false,
-      defaultValue: "-",
-    },
-  ]}
-/>
-
-<StyledSystemProps margin layout background noHeader />
+<StyledSystemProps of={Image} margin layout background noHeader />
diff --git a/src/components/image/image.stories.tsx b/src/components/image/image.stories.tsx
new file mode 100644
index 000000000..43102538c
--- /dev/null
+++ b/src/components/image/image.stories.tsx
@@ -0,0 +1,106 @@
+import React, { useState } from "react";
+import { StoryFn } from "@storybook/react";
+
+import Image from ".";
+import useMediaQuery from "../../hooks/useMediaQuery";
+
+import Box from "../box";
+import Button from "../button";
+import Typography from "../typography";
+import flexibleSvg from "../../../.assets/flexible.svg";
+import pointSvg from "../../../.assets/point.svg";
+import brushSvg from "../../../.assets/brush.svg";
+import collaborateSvg from "../../../.assets/collaborate.svg";
+
+export const DefaultStory: StoryFn = () => (
+  <Image m={3} height="700px" backgroundImage={`url(${flexibleSvg})`}>
+    <Box
+      height="700px"
+      display="flex"
+      alignItems="center"
+      justifyContent="center"
+    >
+      <Typography variant="h4">
+        Here is an example of some overlayed text
+      </Typography>
+    </Box>
+  </Image>
+);
+DefaultStory.parameters = { info: { disable: true } };
+
+export const AsAnImg: StoryFn = () => (
+  <>
+    <Image m={3} ml={8} alt="Example alt text" src={pointSvg} />
+    <Image m={3} ml={5} size="200px" alt="Example alt text" src={brushSvg} />
+    <Image m={3} size="300px" alt="Example alt text" src={collaborateSvg} />
+  </>
+);
+AsAnImg.parameters = { info: { disable: true } };
+
+export const WithHiddenProp = () => {
+  const [isVisible, setIsVisible] = useState(true);
+
+  return (
+    <Box width="20vw">
+      <Button fullWidth onClick={() => setIsVisible((previous) => !previous)}>
+        Toggle image visibility
+      </Button>
+      <Image
+        width="100%"
+        src={flexibleSvg}
+        alt="Curvy line arrow"
+        hidden={!isVisible}
+      />
+    </Box>
+  );
+};
+
+export const CustomResponsiveBehaviour: StoryFn = () => {
+  const query1 = useMediaQuery("(max-width: 1000px)");
+  const query2 = useMediaQuery("(max-width: 900px)");
+  const query3 = useMediaQuery("(max-width: 800px)");
+  const responsiveProps = () => {
+    if (query3) {
+      return {
+        backgroundSize: "contain",
+        backgroundImage: `url(${pointSvg})`,
+        m: 1,
+      };
+    }
+    if (query2) {
+      return {
+        backgroundSize: "30%",
+        backgroundRepeat: "repeat",
+        backgroundImage: `url(${brushSvg})`,
+      };
+    }
+    if (query1) {
+      return {
+        backgroundImage: `url(${collaborateSvg})`,
+        m: 3,
+      };
+    }
+    return {
+      backgroundImage: `url(${flexibleSvg})`,
+      m: 4,
+    };
+  };
+  return (
+    <Image {...responsiveProps()}>
+      <Box
+        height="700px"
+        display="flex"
+        alignItems="center"
+        justifyContent="center"
+      >
+        <Typography variant="h4">
+          Here is an example of some overlayed text
+        </Typography>
+      </Box>
+    </Image>
+  );
+};
+CustomResponsiveBehaviour.parameters = {
+  chromatic: { disable: true },
+  info: { disable: true },
+};
diff --git a/src/components/image/image.style.ts b/src/components/image/image.style.ts
index 26bd29269..25a441811 100644
--- a/src/components/image/image.style.ts
+++ b/src/components/image/image.style.ts
@@ -13,19 +13,24 @@ export interface StyledImageProps
   extends MarginProps,
     BackgroundProps,
     LayoutProps {
-  /** alt property to display when an img fails to load */
+  /** HTML alt property to display when an img fails to load */
   alt?: string;
-  /** any valid file path, passing this will render the component as an img element */
+  /** Any valid file path, passing this will render the component as an img element */
   src?: string;
+  /** HTML hidden property to indicate whether to remain hidden visually and from screen readers */
+  hidden?: boolean;
   /** Children elements, will only render if component is a div element */
   children?: React.ReactNode;
 }
 
-const StyledImage = styled.div.attrs(({ src, children }: StyledImageProps) => ({
-  ...(src && { as: "img" }),
-  children: src ? undefined : children,
-  src,
-}))<StyledImageProps>`
+const StyledImage = styled.div.attrs(
+  ({ src, children, hidden = false }: StyledImageProps) => ({
+    ...(src && { as: "img" }),
+    children: src ? undefined : children,
+    src,
+    hidden,
+  })
+)<StyledImageProps>`
   ${margin}
   ${layout}
 
diff --git a/src/global.d.ts b/src/global.d.ts
index 95ccb7976..ae35f3b67 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -1,2 +1,3 @@
 declare module "@styled-system/prop-types";
 declare module "*.png";
+declare module "*.svg";


Test patch:
diff --git a/src/components/image/image.test.js b/src/components/image/image.test.js
new file mode 100644
index 000000000..5b74502bc
--- /dev/null
+++ b/src/components/image/image.test.js
@@ -0,0 +1,19 @@
+import React from "react";
+import CypressMountWithProviders from "../../../cypress/support/component-helper/cypress-mount";
+
+import Image from ".";
+import carbonLogo from "../../../logo/carbon-logo.png";
+
+context("Image component", () => {
+  it("when hidden prop is false, component should be visible to sighted users", () => {
+    CypressMountWithProviders(<Image src={carbonLogo} alt="Carbon logo" />);
+    cy.get("img").should("be.visible");
+  });
+
+  it("when hidden prop is true, component should not be visible to sighted users", () => {
+    CypressMountWithProviders(
+      <Image src={carbonLogo} alt="Carbon logo" hidden />
+    );
+    cy.get("img").should("not.be.visible");
+  });
+});