Repository: Sage/carbon (ts)

Issue:
Switch: Label is repeated in an unusual format when using VoiceOver in Safari
### Description

Using VoiceOver to read out the content of the Switch component and toggle it on/off.

The content is not read as expected. The label is repeated in the unusual format: `<my label text>and 2 more item`.

The Switch has been tested in Edge using VoiceOver and it is read out as expected without the repeated text.

### Reproduction

Screen recording shared internally on Slack as StackBlitz is not working for Safari

### Steps to reproduce

_No response_

### JIRA ticket numbers (Sage only)

_No response_

### Suggested solution

_No response_

### Carbon version

138.0.2

### Design tokens version

_No response_

### Relevant browsers

Safari

### Relevant OSs

MacOS

### Additional context

_No response_

### Confidentiality

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

Pull request:
fix(switch): fix screen reader announcements with new validation
fix #6901

### 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.
-->

VoiceOver in Safari does not announce "and X more items" when Switch with new validation is focused.
- Input's accessible name is set to the label.
- Input's accessible description is set to the hint text and validation message if provided. 

Required star or (optional) appear on Switch label with new validation. 

### 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.
-->

VoiceOver in Safari announces "and X more items" when Switch with new validation is focused.
Required star or (optional) do not appear on Switch label with new validation. 

### 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
- [ ] All themes are supported if required
- [x] Unit tests added or updated if required
- [ ] Playwright 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
- [ ] 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>
-->

- See [Stackblitz](https://stackblitz.com/edit/parsium-carbon-starter-nnktov?file=src%2FApp.tsx) for issues demo.
- See "new validation" test story for fixes. 
- Check for no visual regressions.  


Patch:
diff --git a/src/components/switch/switch.component.tsx b/src/components/switch/switch.component.tsx
index 43df6f97d..f217a21bf 100644
--- a/src/components/switch/switch.component.tsx
+++ b/src/components/switch/switch.component.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useContext } from "react";
+import React, { useState, useCallback, useContext, useRef } from "react";
 import { MarginProps } from "styled-system";
 
 import StyledSwitch, { ErrorBorder, StyledHintText } from "./switch.style";
@@ -14,6 +14,7 @@ import NewValidationContext from "../carbon-provider/__internal__/new-validation
 import ValidationMessage from "../../__internal__/validation-message/validation-message.component";
 import Box from "../box";
 import Label from "../../__internal__/label";
+import guid from "../../__internal__/utils/helpers/guid";
 
 export interface SwitchProps extends CommonCheckableInputProps, MarginProps {
   /** Identifier used for testing purposes, applied to the root element of the component. */
@@ -57,6 +58,8 @@ export const Switch = React.forwardRef(
       disabled,
       loading,
       reverse = true,
+      required,
+      isOptional,
       validationOnLabel = false,
       labelInline = false,
       labelSpacing,
@@ -80,6 +83,10 @@ export const Switch = React.forwardRef(
     const isControlled = checked !== undefined;
     const { validationRedesignOptIn } = useContext(NewValidationContext);
 
+    const labelId = useRef(`${guid()}-label`);
+    const inputHintId = useRef(`${guid()}-hint`);
+    const validationMessageId = useRef(`${guid()}-message`);
+
     const [checkedInternal, setCheckedInternal] = useState(
       defaultChecked || false
     );
@@ -161,6 +168,8 @@ export const Switch = React.forwardRef(
       reverse: !reverse, // switched to preserve backward compatibility
       validationOnLabel: shouldValidationBeOnLabel && !disabled,
       ref,
+      required,
+      isOptional,
       ...rest,
     };
 
@@ -186,8 +195,8 @@ export const Switch = React.forwardRef(
 
     const inputPropsForNewValidation = {
       autoFocus,
-      error,
-      warning,
+      // set aria-invalid but prevent validationIconId from being added to aria-describedby
+      error: !!error,
       disabled: disabled || loading,
       loading,
       checked: isControlled ? checked : checkedInternal,
@@ -200,37 +209,57 @@ export const Switch = React.forwardRef(
       type: "checkbox",
       role: "switch",
       ref,
+      required,
+      isOptional,
       ...rest,
     };
 
     const applyValidation = error || warning;
 
+    const ariaDescribedBy = [
+      labelHelp && inputHintId.current,
+      applyValidation && validationMessageId.current,
+    ]
+      .filter(Boolean)
+      .join(" ");
+
     return (
       <>
         {validationRedesignOptIn ? (
           <StyledSwitch {...switchStylePropsForNewValidation}>
-            <Label>
-              <Box data-role="hint-text-wrapper" mb={labelHelp ? 0 : 1}>
-                {label}
-                {labelHelp && (
-                  <StyledHintText data-role="hint-text">
-                    {labelHelp}
-                  </StyledHintText>
-                )}
-              </Box>
-              <Box position="relative">
-                <ValidationMessage error={error} warning={warning} />
-                {applyValidation && (
-                  <ErrorBorder
-                    data-role="error-border"
-                    warning={!!(!error && warning)}
-                  />
-                )}
-                <CheckableInput {...inputPropsForNewValidation}>
-                  <SwitchSlider {...switchSliderPropsForNewValidation} />
-                </CheckableInput>
-              </Box>
+            <Label
+              labelId={labelId.current}
+              disabled={disabled}
+              isRequired={required}
+              optional={isOptional}
+            >
+              {label}
             </Label>
+            {labelHelp && (
+              <StyledHintText data-role="hint-text" id={inputHintId.current}>
+                {labelHelp}
+              </StyledHintText>
+            )}
+            <Box position="relative">
+              <ValidationMessage
+                error={error}
+                warning={warning}
+                validationId={validationMessageId.current}
+              />
+              {applyValidation && (
+                <ErrorBorder
+                  data-role="error-border"
+                  warning={!!(!error && warning)}
+                />
+              )}
+              <CheckableInput
+                ariaLabelledBy={`${label && labelId.current}`}
+                ariaDescribedBy={ariaDescribedBy}
+                {...inputPropsForNewValidation}
+              >
+                <SwitchSlider {...switchSliderPropsForNewValidation} />
+              </CheckableInput>
+            </Box>
           </StyledSwitch>
         ) : (
           <TooltipProvider


Test patch:
diff --git a/src/components/switch/switch-test.stories.tsx b/src/components/switch/switch-test.stories.tsx
index ce9469dcb..9b7c9c175 100644
--- a/src/components/switch/switch-test.stories.tsx
+++ b/src/components/switch/switch-test.stories.tsx
@@ -90,6 +90,8 @@ Default.args = {
   value: "test-value",
   disabled: false,
   size: "small",
+  required: false,
+  isOptional: false,
 };
 
 export const NewDefault = ({
@@ -133,6 +135,8 @@ NewDefault.args = {
   value: "test-value",
   disabled: false,
   size: "small",
+  required: false,
+  isOptional: false,
 };
 
 export const WithLongTextStrings = () => (
diff --git a/src/components/switch/switch.test.tsx b/src/components/switch/switch.test.tsx
index 3f1471b17..fdb7bf990 100644
--- a/src/components/switch/switch.test.tsx
+++ b/src/components/switch/switch.test.tsx
@@ -365,17 +365,19 @@ test("renders `labelHelp` as hint text when `validationRedesignOptIn` flag is tr
   expect(screen.getByText("hint text")).toBeVisible();
 });
 
-// Required for coverage
-test("renders the expected margin-bottom value when `labelHelp` is present", () => {
+test("should render with correct accessible name and description when `validationRedesignOptIn` flag is true", () => {
   render(
     <CarbonProvider validationRedesignOptIn>
-      <Switch label="foo" labelHelp="hint text" warning="this is a warning" />
+      <Switch label="foo" labelHelp="hint text" error="this is an error" />
     </CarbonProvider>
   );
 
-  const hintTextWrapper = screen.getByTestId("hint-text-wrapper");
+  const switchElement = screen.getByRole("switch");
 
-  expect(hintTextWrapper).toHaveStyleRule("margin-bottom: var(--spacing000)");
+  expect(switchElement).toHaveAccessibleName("foo");
+  expect(switchElement).toHaveAccessibleDescription(
+    "hint text this is an error"
+  );
 });
 
 test("the expected translations are correctly applied for on", () => {