Repository: Sage/carbon (ts)

Issue:
Update Box component to have a gap prop
### Desired behaviour

Box component to have a prop which enables customisation of the gap property in CSS. See info: https://css-tricks.com/almanac/properties/g/gap/

### Current behaviour

Box has no prop which allows customisation of the gap property in CSS.

### Suggested Solution

_No response_

### 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(box): add support for the gap css property when component is displayed flex or inline-flex FE-5357
fix #5434

### 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.
-->
Adds `gap` prop to the `Box` component. The prop accepts numbers from 0-8 or valid CSS strings. If a number is passed it will be multiplied by 8 to generate a px value. The prop only works when the component has `display` `flex` or `inline-flex`.

![image](https://user-images.githubusercontent.com/44157880/193055557-468bccff-ee5f-4538-985f-fe204d067f02.png)


### Current behaviour

<!--
A clear and concise description of the behaviour before this change.

If applicable, add screenshots. You can paste these directly into GitHub.
-->
No support for `gap` CSS property when `Box` is flex container

### Checklist

<!-- Each PR should include the following -->

- [x] Commits follow our style guide
- [x] Related issues linked in commit messages if required
- [x] Screenshots are included in the PR if useful
- [x] Unit tests added or updated if required
- [ ] Cypress automation tests added or updated if required
- [x] Storybook added or updated if required
- [x] 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

### Additional context

<!-- Add any other context or links about the pull request here. -->

### Testing instructions

<!-- How can a reviewer test this PR? -->

The following CodeSandbox is an example of the broken behaviour.
You can see the new behaviour by looking at the version in the comment by `codesandbox[bot]`.

<!-- Add CodeSandbox here -->
https://codesandbox.io/s/romantic-jackson-wrt9no?file=/src/index.js

Patch:
diff --git a/.storybook/utils/styled-system-props.js b/.storybook/utils/styled-system-props.js
index ceaeba660..21e3f16de 100644
--- a/.storybook/utils/styled-system-props.js
+++ b/.storybook/utils/styled-system-props.js
@@ -425,7 +425,53 @@ const generateStyledSystemLayoutProps = (defaults) => {
   ];
 };
 
-const generateStyledSystemFlexBoxProps = (defaults) => {
+const gapProperties = (hasGap) => {
+  if (!hasGap) return [];
+
+  return [
+    {
+      name: "gap",
+      type: { summary: "number | string" },
+      description:
+        "Sets the gap between children of a flex box container. Accepts any valid CSS string or any number 0 - 8 which will be multiplied by 8 and result in a px value.",
+      required: false,
+      defaultValue: {
+        summary: "-",
+      },
+      table: {
+        category: "Flexbox",
+      },
+    },
+    {
+      name: "columnGap",
+      type: { summary: "number | string" },
+      description:
+        "Sets the column gap between children of a flex box container. Accepts any valid CSS string or any number 0 - 8 which will be multiplied by 8 and result in a px value.",
+      required: false,
+      defaultValue: {
+        summary: "-",
+      },
+      table: {
+        category: "Flexbox",
+      },
+    },
+    {
+      name: "rowGap",
+      type: { summary: "number | string" },
+      description:
+        "Sets the row gap between children of a flex box container. Accepts any valid CSS string or any number 0 - 8 which will be multiplied by 8 and result in a px value.",
+      required: false,
+      defaultValue: {
+        summary: "-",
+      },
+      table: {
+        category: "Flexbox",
+      },
+    },
+  ];
+};
+
+const generateStyledSystemFlexBoxProps = (defaults, hasGap) => {
   return [
     {
       name: "alignItems",
@@ -583,6 +629,7 @@ const generateStyledSystemFlexBoxProps = (defaults) => {
         category: "Flexbox",
       },
     },
+    ...gapProperties(hasGap),
   ];
 };
 
@@ -723,6 +770,7 @@ const StyledSystemProps = ({
   width,
   layout,
   flexBox,
+  hasGapProps,
   defaults = {},
   noHeader,
   margin,
@@ -751,7 +799,7 @@ const StyledSystemProps = ({
     rows.push(...generateStyledSystemWidthProps(defaults));
   }
   if (flexBox) {
-    rows.push(...generateStyledSystemFlexBoxProps(defaults));
+    rows.push(...generateStyledSystemFlexBoxProps(defaults, hasGapProps));
   }
   if (background) {
     rows.push(...generateStyledSystemBackgroundProps(defaults));
diff --git a/src/components/box/box.component.tsx b/src/components/box/box.component.tsx
index 1f6d6527b..298fffffe 100644
--- a/src/components/box/box.component.tsx
+++ b/src/components/box/box.component.tsx
@@ -15,9 +15,13 @@ import BaseTheme from "../../style/themes/base";
 import styledColor from "../../style/utils/color";
 import boxConfig from "./box.config";
 
+const GAP_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8] as const;
+
 export type OverflowWrap = "break-word" | "anywhere";
 export type ScrollVariant = "light" | "dark";
 export type BoxSizing = "content-box" | "border-box";
+export type AllowedNumericalValues = typeof GAP_VALUES[number];
+export type Gap = AllowedNumericalValues | string;
 
 export interface BoxProps
   extends SpaceProps,
@@ -35,6 +39,12 @@ export interface BoxProps
   boxSizing?: BoxSizing;
   /** Allows a tabindex to be specified */
   tabIndex?: number | string;
+  /** Gap, an integer multiplier of the base spacing constant (8px) or any valid CSS string." */
+  gap?: Gap;
+  /** Column gap, an integer multiplier of the base spacing constant (8px) or any valid CSS string." */
+  columnGap?: Gap;
+  /** Row gap an integer multiplier of the base spacing constant (8px) or any valid CSS string." */
+  rowGap?: Gap;
 }
 
 const Box = styled.div<BoxProps>`
@@ -42,6 +52,7 @@ const Box = styled.div<BoxProps>`
   ${layout}
   ${flexbox}
   ${position}
+
   ${({ color, bg, backgroundColor, ...rest }) =>
     styledColor({ color, bg, backgroundColor, ...rest })}
 
@@ -73,6 +84,25 @@ const Box = styled.div<BoxProps>`
     css`
       box-sizing: ${boxSizing};
     `}
+
+    ${({ display, gap, columnGap, rowGap }) =>
+    (display === "flex" || display === "inline-flex") &&
+    css`
+      ${gap !== undefined &&
+      css`
+        gap: ${boxConfig.gap(gap)};
+      `}
+
+      ${columnGap !== undefined &&
+      css`
+        column-gap: ${boxConfig.gap(columnGap)};
+      `}
+
+      ${rowGap !== undefined &&
+      css`
+        row-gap: ${boxConfig.gap(rowGap)};
+      `}
+    `};
 `;
 
 Box.defaultProps = {
diff --git a/src/components/box/box.config.ts b/src/components/box/box.config.ts
index d09e50ab7..0c55744b3 100644
--- a/src/components/box/box.config.ts
+++ b/src/components/box/box.config.ts
@@ -1,3 +1,5 @@
+import { space } from "../../style/themes/base/base-theme.config";
+
 export default {
   light: {
     thumb: "var(--colorsUtilityMajor300)",
@@ -8,4 +10,12 @@ export default {
     thumb: "var(--colorsUtilityMajor200)",
     track: "var(--colorsUtilityMajor400)",
   },
+
+  gap: (gapValue: number | string) => {
+    if (typeof gapValue === "number") {
+      return space[gapValue];
+    }
+
+    return gapValue;
+  },
 };
diff --git a/src/components/box/box.spec.tsx b/src/components/box/box.spec.tsx
index 29367d810..0a39e4f7a 100644
--- a/src/components/box/box.spec.tsx
+++ b/src/components/box/box.spec.tsx
@@ -8,9 +8,20 @@ import {
   testStyledSystemPosition,
   assertStyleMatch,
 } from "../../__spec_helper__/test-utils";
-import Box, { OverflowWrap, ScrollVariant, BoxSizing } from "./box.component";
+import Box, {
+  OverflowWrap,
+  ScrollVariant,
+  BoxSizing,
+  AllowedNumericalValues,
+  Gap,
+} from "./box.component";
 import boxConfig from "./box.config";
 
+const GAP_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8] as AllowedNumericalValues[];
+
+const getGapValue = (gap: number | string) =>
+  typeof gap === "number" ? `var(--spacing${gap}00)` : gap;
+
 describe("Box", () => {
   testStyledSystemSpacing((props) => <Box {...props} />);
   testStyledSystemColor((props) => <Box {...props} />);
@@ -71,4 +82,31 @@ describe("Box", () => {
       assertStyleMatch({ boxSizing }, wrapper);
     }
   );
+
+  it.each<Gap>([...GAP_VALUES, "20px", "20%", "20px 20%"])(
+    "has styles applied when gap is set to %s",
+    (gapValue) => {
+      const wrapper = mount(<Box display="flex" gap={gapValue} />);
+      const gap = getGapValue(gapValue);
+      assertStyleMatch({ gap }, wrapper);
+    }
+  );
+
+  it.each<Gap>([...GAP_VALUES, "20px", "20%"])(
+    "has styles applied when row-gap is set to %s",
+    (gapValue) => {
+      const wrapper = mount(<Box display="flex" rowGap={gapValue} />);
+      const rowGap = getGapValue(gapValue);
+      assertStyleMatch({ rowGap }, wrapper);
+    }
+  );
+
+  it.each<Gap>([...GAP_VALUES, "20px", "20%"])(
+    "has styles applied when column-gap is set to %s",
+    (gapValue) => {
+      const wrapper = mount(<Box display="flex" columnGap={gapValue} />);
+      const columnGap = getGapValue(gapValue);
+      assertStyleMatch({ columnGap }, wrapper);
+    }
+  );
 });
diff --git a/src/components/box/box.stories.mdx b/src/components/box/box.stories.mdx
index 97251c71e..de7409b0e 100644
--- a/src/components/box/box.stories.mdx
+++ b/src/components/box/box.stories.mdx
@@ -119,6 +119,115 @@ Please note that in order to see the `position: fixed` Box, you will need to vie
   </Story>
 </Canvas>
 
+### Gap
+
+The `Box` component also supports the `gap`, `column-gap` and `row-gap` properties when its `display` property is set to either 
+`flex` or `inline-flex`. See the [prop table](#props) below for more information on the types it accepts. Below is an example of 
+how these props can be used.
+
+<Canvas>
+  <Story name="flex gap" parameters={{ info: { disable: true } }}>
+    <Box
+      display="flex"
+      flexDirection="column"
+      gap={2}
+    >
+      <Box
+        display="flex"
+        columnGap={1}
+      >
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={2}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={3}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={4}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={5}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={6}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={7}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+        <Box
+          display="flex"
+          flexDirection="column"
+          rowGap={8}
+        >
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+          <Box width="100px" height="100px" bg="primary"></Box>
+        </Box>
+      </Box>
+      <Box
+        display="flex"
+        gap={4}
+      >
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+      </Box>
+      <Box
+        display="flex"
+        gap={8}
+      >
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+      </Box>
+      <Box
+        display="flex"
+        gap="72px"
+      >
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+        <Box width="100px" height="100px" bg="primary"></Box>
+      </Box>
+    </Box>
+  </Story>
+</Canvas>
+
 ### Layout
 
 <Canvas>
@@ -259,9 +368,7 @@ If no `scrollVariant` prop is set then the scroll bar will resort to the browser
 
 ### Style System
 
-<StyledSystemProps spacing color flexBox layout position noHeader />
-
-### Scroll Variant
+<StyledSystemProps spacing color flexBox hasGapProps layout position noHeader />
 
 <ArgsTable
   rows={[
@@ -279,5 +386,17 @@ If no `scrollVariant` prop is set then the scroll bar will resort to the browser
       description: "String to denote which style variant to use for scrollbars",
       required: false,
     },
+    {
+      name: "boxSizing",
+      type: { summary: "content-box | border-box" },
+      description: "Set the box-sizing attribute of the Box component",
+      required: false,
+    },
+    {
+      name: "tabIndex",
+      type: { summary: "number | string" },
+      description: "Allows a tabindex to be specified",
+      required: false,
+    },
   ]}
 />


Test patch:
diff --git a/src/components/box/box.test.js b/src/components/box/box.test.js
index f431e7229..30d6c532a 100644
--- a/src/components/box/box.test.js
+++ b/src/components/box/box.test.js
@@ -748,5 +748,58 @@ context("Testing Box component", () => {
         getDataElementByValue("box").should("have.css", "position", value);
       }
     );
+
+    it.each([0, 1, 2, 3, 4, 5, 6, 7, 8, "20%", "20px"])(
+      "should verify gap prop sets expected CSS on Box",
+      (gap) => {
+        CypressMountWithProviders(<BoxComponent display="flex" gap={gap} />);
+
+        getDataElementByValue("box").should(
+          "have.css",
+          "row-gap",
+          typeof gap === "number" ? `${gap * 8}px` : gap
+        );
+
+        getDataElementByValue("box").should(
+          "have.css",
+          "column-gap",
+          typeof gap === "number" ? `${gap * 8}px` : gap
+        );
+      }
+    );
+
+    it.each([0, 1, 2, 3, 4, 5, 6, 7, 8, "20%", "20px"])(
+      "should verify rowGap prop sets expected CSS on Box and overrides gap if set",
+      (rowGap) => {
+        CypressMountWithProviders(
+          <BoxComponent display="flex" rowGap={rowGap} gap={8} />
+        );
+
+        getDataElementByValue("box").should(
+          "have.css",
+          "row-gap",
+          typeof rowGap === "number" ? `${rowGap * 8}px` : rowGap
+        );
+
+        getDataElementByValue("box").should("have.css", "column-gap", "64px");
+      }
+    );
+
+    it.each([0, 1, 2, 3, 4, 5, 6, 7, 8, "20%", "20px"])(
+      "should verify columnGap prop sets expected CSS on Box and overrides gap if set",
+      (columnGap) => {
+        CypressMountWithProviders(
+          <BoxComponent display="flex" columnGap={columnGap} gap={8} />
+        );
+
+        getDataElementByValue("box").should("have.css", "row-gap", "64px");
+
+        getDataElementByValue("box").should(
+          "have.css",
+          "column-gap",
+          typeof columnGap === "number" ? `${columnGap * 8}px` : columnGap
+        );
+      }
+    );
   });
 });