Repository: Sage/carbon (ts)

Issue:
Loader spinner - custom text should be one line and centered
### Description

If i override the label of a Loader Spinner with a long text, it is left align and truncated to take the width of the spinner

![image](https://github.com/Sage/carbon/assets/16415182/7589bc5d-00aa-4aa2-adf9-81105f9f7d2d)


### Suggested solution

The text should be centered on one line.

![image](https://github.com/Sage/carbon/assets/16415182/e8bfac4b-debd-4d36-b8ff-b600c21835e5)


### Demo URL

_No response_

### Alternatives

_No response_

### Additional context

_No response_

### Confidentiality

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

Pull request:
fix(loader-spinner): allow components label to extend longer than the svg wrapper
fix #6702 

### Proposed behaviour

<!--
A clear and concise description of what changes this PR makes. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

All fixed widths on the component have been removed, making the component center upon rendering and also making the component much more responsive and flexible. The component's label can now exceed the length of the SVG wrapper above. Also, margin-top's are now applied to all sizes above extra-small to match designs, and text now aligns fully to the center. 

![Screenshot 2024-05-02 at 11 42 44](https://github.com/Sage/carbon/assets/114918852/8d3a2510-c9a7-4f0a-8e31-a863261f05ab)

![Screenshot 2024-05-02 at 11 43 01](https://github.com/Sage/carbon/assets/114918852/d1447683-76ef-482e-bb13-11f2f2a4d532)

![Screenshot 2024-05-02 at 11 43 14](https://github.com/Sage/carbon/assets/114918852/64b4edb6-0ed1-4ae6-904c-3dfd2f31e829)

### Current behaviour

<!--
A clear and concise description of the behaviour before this change. If applicable, include any UI screenshots to help explain your request. If you are a Sage contributor, please DO NOT share any commercially sensitive information, such as UI screenshots or code from Sage products.
-->

Currently, the component's label and SVG wrapper both have a set width, which corresponds to each component's size. This is suitable for smaller labels such as `"Loading..."`, `"Saving..."` etc. However, some designs require longer strings and shorter words in `en-GB` may be longer when translated.

### 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
- [ ] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Playwright 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
- [ ] Typescript `d.ts` file added or updated if required
- [x] Related docs have been updated if required

#### QA

- [ ] Tested in provided StackBlitz sandbox/Storybook
- [ ] Add new Playwright 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?

If this PR addresses a pre-existing bug, please include a link to a sandbox that reproduces the original bug. A starter template has been provided to help you do this:
<https://stackblitz.com/fork/github/Parsium/carbon-starter>
-->


Patch:
diff --git a/src/components/loader-spinner/loader-spinner.config.ts b/src/components/loader-spinner/loader-spinner.config.ts
index 36dc4f0ab..c4d651cdb 100644
--- a/src/components/loader-spinner/loader-spinner.config.ts
+++ b/src/components/loader-spinner/loader-spinner.config.ts
@@ -23,15 +23,20 @@ type LoaderSpinnerSizeParams = Record<
   {
     wrapperDimensions: number;
     strokeWidth: number;
+    labelMarginTop?: number;
   }
 >;
 
 const LOADER_SPINNER_SIZE_PARAMS: LoaderSpinnerSizeParams = {
   "extra-small": { wrapperDimensions: 20, strokeWidth: 4 },
-  small: { wrapperDimensions: 32, strokeWidth: 4 },
-  medium: { wrapperDimensions: 56, strokeWidth: 3.3 },
-  large: { wrapperDimensions: 80, strokeWidth: 3.7 },
-  "extra-large": { wrapperDimensions: 104, strokeWidth: 3.7 },
+  small: { wrapperDimensions: 32, strokeWidth: 4, labelMarginTop: 12 },
+  medium: { wrapperDimensions: 56, strokeWidth: 3.3, labelMarginTop: 16 },
+  large: { wrapperDimensions: 80, strokeWidth: 3.7, labelMarginTop: 22 },
+  "extra-large": {
+    wrapperDimensions: 104,
+    strokeWidth: 3.7,
+    labelMarginTop: 26,
+  },
 };
 
 export type { LoaderSpinnerSizes, LoaderSpinnerVariants };
diff --git a/src/components/loader-spinner/loader-spinner.mdx b/src/components/loader-spinner/loader-spinner.mdx
index 8cdaa2367..7ab054d04 100644
--- a/src/components/loader-spinner/loader-spinner.mdx
+++ b/src/components/loader-spinner/loader-spinner.mdx
@@ -33,7 +33,7 @@ Can be used to demonstrate a known or unknown wait time.
 Import `LoaderSpinner` into the project.
 
 ```javascript
-import LoaderSpinner from "carbon-react/lib/components/loader-spinner";
+import { LoaderSpinner } from "carbon-react/lib/components/loader-spinner";
 ```
 
 ## Examples
diff --git a/src/components/loader-spinner/loader-spinner.pw.tsx b/src/components/loader-spinner/loader-spinner.pw.tsx
index b37d80bfc..5cac8851b 100644
--- a/src/components/loader-spinner/loader-spinner.pw.tsx
+++ b/src/components/loader-spinner/loader-spinner.pw.tsx
@@ -63,7 +63,7 @@ test.describe("Prop checks for Loader Spinner component", () => {
   );
 
   sizes.forEach((spinnerSizes) => {
-    test(`when the 'size' prop is passed as '${spinnerSizes}' the svg circle wrapper has the correct dimensions`, async ({
+    test(`when the 'size' prop is passed as '${spinnerSizes}' the svg circle wrapper has the correct height`, async ({
       mount,
       page,
     }) => {
@@ -73,15 +73,11 @@ test.describe("Prop checks for Loader Spinner component", () => {
         "height",
         `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
       );
-      await expect(loaderSpinnerSvg(page)).toHaveCSS(
-        "width",
-        `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
-      );
     });
   });
 
   sizes.forEach((spinnerSizes) => {
-    test(`when the 'size' prop is passed as '${spinnerSizes}' the svg circle wrapper has the correct minimum dimensions`, async ({
+    test(`when the 'size' prop is passed as '${spinnerSizes}' the svg circle wrapper has the correct minimum height`, async ({
       mount,
       page,
     }) => {
@@ -91,10 +87,6 @@ test.describe("Prop checks for Loader Spinner component", () => {
         "min-height",
         `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
       );
-      await expect(loaderSpinnerSvg(page)).toHaveCSS(
-        "min-width",
-        `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
-      );
     });
   });
 
@@ -140,15 +132,15 @@ test.describe("Prop checks for Loader Spinner component", () => {
 
   (["small", "medium", "large", "extra-large"] as const).forEach(
     (spinnerSizes) => {
-      test(`when the 'size' prop is passed as '${spinnerSizes}' the label has the correct width`, async ({
+      test(`when the 'size' prop is passed as '${spinnerSizes}' the label has the correct margin-top`, async ({
         mount,
         page,
       }) => {
         await mount(<LoaderSpinnerComponent size={spinnerSizes} />);
 
         await expect(loaderSpinnerVisibleLabel(page)).toHaveCSS(
-          "width",
-          `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
+          "margin-top",
+          `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].labelMarginTop}px`
         );
       });
     }
diff --git a/src/components/loader-spinner/loader-spinner.spec.tsx b/src/components/loader-spinner/loader-spinner.spec.tsx
index 99229f321..6208fd733 100644
--- a/src/components/loader-spinner/loader-spinner.spec.tsx
+++ b/src/components/loader-spinner/loader-spinner.spec.tsx
@@ -100,14 +100,11 @@ describe("LoaderSpinner", () => {
     );
 
     it.each(sizes)(
-      "when the 'size' prop is passed as `%s` the svg circle wrapper has the correct dimensions",
+      "when the 'size' prop is passed as `%s` the svg circle wrapper has the correct height",
       (spinnerSizes) => {
         render(<LoaderSpinner size={spinnerSizes} />);
         const svgCircleElement = screen.getByRole("presentation");
 
-        expect(svgCircleElement).toHaveStyle(
-          `width: ${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
-        );
         expect(svgCircleElement).toHaveStyle(
           `height: ${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
         );
@@ -115,14 +112,11 @@ describe("LoaderSpinner", () => {
     );
 
     it.each(sizes)(
-      "when the 'size' prop is passed as `%s` the svg circle wrapper has the correct minimum dimensions",
+      "when the 'size' prop is passed as `%s` the svg circle wrapper has the correct minimum height",
       (spinnerSizes) => {
         render(<LoaderSpinner size={spinnerSizes} />);
         const svgCircleElement = screen.getByRole("presentation");
 
-        expect(svgCircleElement).toHaveStyle(
-          `min-width: ${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
-        );
         expect(svgCircleElement).toHaveStyle(
           `min-height: ${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
         );
@@ -168,14 +162,14 @@ describe("LoaderSpinner", () => {
       "large",
       "extra-large",
     ] as LoaderSpinnerSizes[])(
-      "when the 'size' prop is passed as `%s` the label has the correct width",
+      "when the 'size' prop is passed as `%s` the label has the correct margin-top",
       (spinnerSizes) => {
         render(<LoaderSpinner size={spinnerSizes} />);
         const visibleLabelElement = screen.getByTestId("visible-label");
 
-        expect(visibleLabelElement).toHaveStyle(
-          `width: ${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].wrapperDimensions}px`
-        );
+        expect(visibleLabelElement).toHaveStyle({
+          marginTop: `${LOADER_SPINNER_SIZE_PARAMS[spinnerSizes].labelMarginTop}px`,
+        });
       }
     );
 
diff --git a/src/components/loader-spinner/loader-spinner.stories.tsx b/src/components/loader-spinner/loader-spinner.stories.tsx
index 5edc5c7a1..bff54df6e 100644
--- a/src/components/loader-spinner/loader-spinner.stories.tsx
+++ b/src/components/loader-spinner/loader-spinner.stories.tsx
@@ -22,13 +22,22 @@ const meta: Meta<typeof LoaderSpinner> = {
 export default meta;
 type Story = StoryObj<typeof LoaderSpinner>;
 
-export const Default: Story = () => <LoaderSpinner />;
+export const Default: Story = () => (
+  <Box display="flex">
+    <LoaderSpinner />
+  </Box>
+);
 Default.storyName = "Default";
 
 export const OverrideSpinnerLabel: Story = () => (
   <Box display="flex">
     <LoaderSpinner mx="3" spinnerLabel="Processing..." variant="action" />
     <LoaderSpinner mx="3" spinnerLabel="Saving..." variant="neutral" />
+    <LoaderSpinner
+      mx="3"
+      spinnerLabel="Loading... This can take a few seconds... Or a few minutes..."
+      variant="action"
+    />
   </Box>
 );
 OverrideSpinnerLabel.storyName = "Override Spinner Label";
@@ -45,7 +54,9 @@ export const Sizes: Story = () => {
 Sizes.storyName = "Sizes";
 
 export const ShowSpinnerLabel: Story = () => (
-  <LoaderSpinner showSpinnerLabel={false} />
+  <Box display="flex">
+    <LoaderSpinner showSpinnerLabel={false} />
+  </Box>
 );
 ShowSpinnerLabel.storyName = "Show Spinner Label";
 
@@ -82,7 +93,11 @@ export const HasMotion: Story = () => (
 );
 HasMotion.storyName = "Has Motion";
 
-export const IsTracked: Story = () => <LoaderSpinner isTracked />;
+export const IsTracked: Story = () => (
+  <Box display="flex">
+    <LoaderSpinner isTracked />
+  </Box>
+);
 IsTracked.storyName = "Is Tracked";
 
 export const AnimationTime: Story = () => (
diff --git a/src/components/loader-spinner/loader-spinner.style.ts b/src/components/loader-spinner/loader-spinner.style.ts
index cf84e2444..91aa9191a 100644
--- a/src/components/loader-spinner/loader-spinner.style.ts
+++ b/src/components/loader-spinner/loader-spinner.style.ts
@@ -38,9 +38,10 @@ export const StyledLabel = styled(Typography)<
     css`
       display: flex;
       justify-content: center;
-      ${size !== "extra-small" &&
-      `width: ${LOADER_SPINNER_SIZE_PARAMS[size].wrapperDimensions}px`};
-      ${size === "extra-small" && `margin-left: var(--spacing100)`};
+      text-align: center;
+      ${size === "extra-small"
+        ? "margin-left: var(--spacing100)"
+        : `margin-top: ${LOADER_SPINNER_SIZE_PARAMS[size].labelMarginTop}px`};
     `}
 `;
 
@@ -64,9 +65,7 @@ export const StyledSpinnerCircleSvg = styled.svg<StyledSpinnerCircleSvgProps>`
     return (
       size &&
       css`
-        width: ${dimensions};
         height: ${dimensions};
-        min-width: ${dimensions};
         min-height: ${dimensions};
 
         circle[data-role="outer-arc"] {


Test patch:
diff --git a/src/components/loader-spinner/loader-spinner-test.stories.tsx b/src/components/loader-spinner/loader-spinner-test.stories.tsx
index 499b75e74..87d5aac1f 100644
--- a/src/components/loader-spinner/loader-spinner-test.stories.tsx
+++ b/src/components/loader-spinner/loader-spinner-test.stories.tsx
@@ -57,7 +57,7 @@ export default {
 };
 
 export const Default = (props: Partial<LoaderSpinnerProps>) => (
-  <Box p={3} backgroundColor="darkgrey" width="200px" height="200px">
+  <Box p={3} backgroundColor="darkgrey" width="100%" height="200px">
     <LoaderSpinner {...props} />
   </Box>
 );