Repository: Sage/carbon (ts)

Issue:
<Search> component - allow consumers to target props for the "X" button
### Description

As a developer using the `<Search>` component 
I want the ability to be able to programatically target the "X" button through props.
So that I am able to programmatically hide this element for non-visual users. 




### Suggested solution

Context: 
During testing of a new piece of UI that contained a `<Search>` component, one of our QA’s questioned how the “X” button was read out using VoiceOver and keyboard navigation. It is read out as “clickable” (can be seen in the Carbon docs too).

After speaking to the Accessibility team, he highlighted that users can clear the text using the keyboard/voice commands so perhaps this button being read out is unnecessary for non-visual users. They’ve suggested a solution could be to programmatically hide the “X” button using `aria-hidden`. But at present these props are handled internally by the `<Search>` component and are not editable.

### Demo URL

https://stackblitz.com/edit/parsium-carbon-starter-6lgyawsc?file=src%2FApp.tsx

### Alternatives

_No response_

### Additional context

_No response_

### Confidentiality

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

Pull request:
feat(search, menu-item): Search accessibility updates
Users can clear the text of the search box using the keyboard/voice commands; as such, the clear button in the search input is being read out to non-visual users unnecessarily and can be removed from the tab order. It also does not need to be focusable. In order to enhance this component further, functionality has been introduced to allow users to clear the Search field with the Escape key, which is standard practice across the web.

Resolves #7170

### Proposed behaviour

- Remove the clear button from the tab order;
- Add the `aria-hidden` attribute to the clear button;
- Enable support for clearing the input via the Escape key

https://github.com/user-attachments/assets/8fa5ca88-9596-40e8-ab6f-adb915c2356d

https://github.com/user-attachments/assets/c3d4c682-ef23-4267-bd0f-43da4bd8a429

### Current behaviour

The "clear" icon is tabbable and read out by screen readers, and the search input cannot be cleared by expected means.

<img width="818" alt="Screenshot 2025-01-30 at 06 55 31" src="https://github.com/user-attachments/assets/5670356c-d439-4f20-bf38-a186ee984670" />

https://github.com/user-attachments/assets/0c081fd2-b3a8-42a2-ac8f-79da41c92f54

### Checklist

- [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] 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


Patch:
diff --git a/src/__internal__/input-icon-toggle/input-icon-toggle.component.tsx b/src/__internal__/input-icon-toggle/input-icon-toggle.component.tsx
index 028d2f6c7..ecba71faa 100644
--- a/src/__internal__/input-icon-toggle/input-icon-toggle.component.tsx
+++ b/src/__internal__/input-icon-toggle/input-icon-toggle.component.tsx
@@ -18,7 +18,6 @@ export interface InputIconToggleProps
   onFocus?: (ev: React.FocusEvent<HTMLElement>) => void;
   onMouseDown?: (ev: React.MouseEvent<HTMLElement>) => void;
   readOnly?: boolean;
-  tooltipId?: string;
   useValidationIcon?: boolean;
   /** Id of the validation icon */
   validationIconId?: string;
@@ -83,11 +82,12 @@ const InputIconToggle = ({
         onFocus={onFocus}
         onBlur={onBlur}
         onMouseDown={onMouseDown}
-        tabIndex={iconTabIndex}
         data-element="input-icon-toggle"
         disabled={disabled}
         readOnly={readOnly}
         data-role="input-icon-toggle"
+        aria-hidden="true"
+        tabIndex={-1}
       >
         <Icon disabled={disabled || readOnly} type={type} />
       </InputIconToggleStyle>
diff --git a/src/components/menu/menu-item/menu-item.component.tsx b/src/components/menu/menu-item/menu-item.component.tsx
index 9760f9414..5ef8b7455 100644
--- a/src/components/menu/menu-item/menu-item.component.tsx
+++ b/src/components/menu/menu-item/menu-item.component.tsx
@@ -230,24 +230,6 @@ export const MenuItem = ({
         (ref.current as HTMLElement)?.focus();
       }
 
-      const inputIcon = ref.current?.querySelector(
-        "[data-element='input-icon-toggle']",
-      );
-
-      const shouldFocusIcon =
-        inputIcon?.getAttribute("tabindex") === "0" &&
-        document.activeElement === inputRef.current &&
-        inputRef.current?.value;
-
-      // let natural tab order move focus if input icon is tabbable or input with button exists
-      if (
-        Events.isTabKey(event) &&
-        ((!Events.isShiftKey(event) && shouldFocusIcon) ||
-          (Events.isShiftKey(event) && document.activeElement === inputIcon))
-      ) {
-        return;
-      }
-
       if (handleSubmenuKeyDown) {
         handleSubmenuKeyDown(event);
       }
diff --git a/src/components/menu/menu.pw.tsx b/src/components/menu/menu.pw.tsx
index 6361320c9..7de567af8 100644
--- a/src/components/menu/menu.pw.tsx
+++ b/src/components/menu/menu.pw.tsx
@@ -29,7 +29,6 @@ import {
 } from "../../../playwright/components/menu/index";
 import {
   searchDefaultInput,
-  searchCrossIcon,
   searchButton,
 } from "../../../playwright/components/search/index";
 import {
@@ -387,59 +386,6 @@ test.describe("Prop tests for Menu component", () => {
     });
   });
 
-  test(`should verify the Search component close icon is focusable when using keyboard to navigate down the list of items`, async ({
-    mount,
-    page,
-  }) => {
-    await mount(<MenuComponentSearch />);
-
-    await page.keyboard.press("Tab");
-    await page.keyboard.press("Enter");
-    await page.keyboard.press("ArrowDown");
-    await searchDefaultInput(page).fill("FooBar");
-    await page.keyboard.press("Tab");
-    const cross = searchCrossIcon(page).locator("..");
-    await expect(cross).toBeFocused();
-  });
-
-  test(`should verify the Search component close icon is centred when focused`, async ({
-    mount,
-    page,
-  }) => {
-    await mount(<MenuComponentSearch />);
-    const bottomLess = 220;
-    const topLess = 184;
-    const leftLess = 108;
-    // additionVal is to compensate for the outline.
-    const additionVal = 6;
-
-    await page.keyboard.press("Tab");
-    await page.keyboard.press("Enter");
-    await page.keyboard.press("ArrowDown");
-    await searchDefaultInput(page).fill("FooBar");
-    await page.keyboard.press("Tab");
-    const cross = searchCrossIcon(page).locator("..");
-    await expect(cross).toBeFocused();
-
-    const boundBottom = await cross.evaluate((element) => {
-      return element.getBoundingClientRect().bottom;
-    });
-    expect(boundBottom).toBeLessThanOrEqual(bottomLess + additionVal);
-    expect(boundBottom).toBeGreaterThan(bottomLess);
-
-    const boundTop = await cross.evaluate((element) => {
-      return element.getBoundingClientRect().top;
-    });
-    expect(boundTop).toBeLessThanOrEqual(topLess + additionVal);
-    expect(boundTop).toBeGreaterThan(topLess);
-
-    const boundLeft = await cross.evaluate((element) => {
-      return element.getBoundingClientRect().left;
-    });
-    expect(boundLeft).toBeLessThanOrEqual(leftLess + additionVal);
-    expect(boundLeft).toBeGreaterThan(leftLess);
-  });
-
   test(`should verify that the Search component is focusable by using the downarrow key when rendered as the parent of a scrollable submenu`, async ({
     mount,
     page,
@@ -474,7 +420,6 @@ test.describe("Prop tests for Menu component", () => {
     await page.keyboard.press("Enter");
     await page.keyboard.press("ArrowDown");
     await searchDefaultInput(page).fill("FooBar");
-    await page.keyboard.press("Tab");
     await page.keyboard.press("Enter");
     const subMenuBlock = submenuBlock(page).first().locator("li").first();
     await expect(subMenuBlock).toBeVisible();
@@ -1450,7 +1395,7 @@ test.describe("Prop tests for Menu Fullscreen component", () => {
     await expect(item2).toBeFocused();
   });
 
-  test(`should focus the search icon and button on tab press when the current item has a Search input with searchButton and has a value`, async ({
+  test(`should focus the search button on tab press when the current item has a Search input with searchButton and has a value`, async ({
     mount,
     page,
   }) => {
@@ -1463,9 +1408,6 @@ test.describe("Prop tests for Menu Fullscreen component", () => {
     const searchInput = searchDefaultInput(page);
     await expect(searchInput).toBeFocused();
     await page.keyboard.press("Tab");
-    const crossIcon = searchCrossIcon(page).locator("..");
-    await expect(crossIcon).toBeFocused();
-    await page.keyboard.press("Tab");
     const button = searchButton(page);
     await expect(button).toBeFocused();
     await expect(button).toHaveCSS(
diff --git a/src/components/search/search.component.tsx b/src/components/search/search.component.tsx
index 97a3bd23b..86bdc21f0 100644
--- a/src/components/search/search.component.tsx
+++ b/src/components/search/search.component.tsx
@@ -10,6 +10,7 @@ import Button from "../button";
 import { ValidationProps } from "../../__internal__/validations";
 import Logger from "../../__internal__/utils/logger";
 import useLocale from "../../hooks/__internal__/useLocale";
+import Events from "../../__internal__/utils/helpers/events";
 
 export interface SearchEvent {
   target: {
@@ -220,6 +221,18 @@ export const Search = React.forwardRef<SearchHandle, SearchProps>(
         event.stopPropagation();
       }
 
+      if (Events.isEscKey(event) && !isSearchValueEmpty) {
+        event.stopPropagation();
+        setSearchValue("");
+        onChange?.({
+          target: {
+            ...(name && { name }),
+            ...(id && { id }),
+            value: "",
+          },
+        });
+      }
+
       if (onKeyDown) {
         onKeyDown(event);
       }
diff --git a/src/components/search/search.pw.tsx b/src/components/search/search.pw.tsx
index ba18c58fa..bb1744030 100644
--- a/src/components/search/search.pw.tsx
+++ b/src/components/search/search.pw.tsx
@@ -68,24 +68,6 @@ test.describe("When focused", () => {
       "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px",
     );
   });
-
-  test("should have the expected styling for the cross icon", async ({
-    mount,
-    page,
-  }) => {
-    await mount(<SearchComponent searchButton />);
-
-    const searchDefaultInputElement = searchDefaultInput(page);
-    await searchDefaultInputElement.clear();
-    await searchDefaultInputElement.fill(testDataStandard);
-    await searchDefaultInputElement.press("Tab");
-    const searchCrossIconElementParent = searchCrossIcon(page).locator("..");
-
-    await expect(searchCrossIconElementParent).toHaveCSS(
-      "box-shadow",
-      "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px",
-    );
-  });
 });
 
 test.describe("Prop tests for Search component", () => {
diff --git a/src/components/textbox/textbox.pw.tsx b/src/components/textbox/textbox.pw.tsx
index b84400001..81db769d0 100644
--- a/src/components/textbox/textbox.pw.tsx
+++ b/src/components/textbox/textbox.pw.tsx
@@ -629,14 +629,6 @@ test.describe("Prop checks for Textbox component", () => {
     });
   });
 
-  test("should render with iconTabIndex prop", async ({ mount, page }) => {
-    await mount(<TextboxComponent inputIcon="add" iconTabIndex={25} />);
-
-    const inputIcon = getDataElementByValue(page, "input-icon-toggle");
-
-    await expect(inputIcon).toHaveAttribute("tabindex", "25");
-  });
-
   (
     ["top", "bottom", "left", "right"] as TextboxProps["tooltipPosition"][]
   ).forEach((position) => {


Test patch:
diff --git a/src/components/menu/menu-item/menu-item.test.tsx b/src/components/menu/menu-item/menu-item.test.tsx
index 1cdb9ec83..674d7c850 100644
--- a/src/components/menu/menu-item/menu-item.test.tsx
+++ b/src/components/menu/menu-item/menu-item.test.tsx
@@ -722,8 +722,7 @@ describe("when MenuItem has a submenu", () => {
     await user.tab();
     expect(screen.getByDisplayValue("foo")).toHaveFocus();
     await user.tab();
-    expect(screen.getByTestId("input-icon-toggle")).toHaveFocus();
-    await user.tab();
+    expect(screen.getByTestId("input-icon-toggle")).not.toHaveFocus();
     expect(submenuItems[1]).toHaveFocus();
   });
 
diff --git a/src/components/menu/menu-test.stories.tsx b/src/components/menu/menu-test.stories.tsx
index 2b410bbd4..16d38067f 100644
--- a/src/components/menu/menu-test.stories.tsx
+++ b/src/components/menu/menu-test.stories.tsx
@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from "react";
 import { Meta } from "@storybook/react";
 import { action } from "@storybook/addon-actions";
+
 import { allModes } from "../../../.storybook/modes";
 import isChromatic from "../../../.storybook/isChromatic";
 import {
@@ -39,6 +40,7 @@ const meta: Meta<typeof Menu> = {
     "WhenMenuItemsWrap",
     "MenuFullScreenWithMaxWidth",
     "IconAlignment",
+    "TabbingOrder",
   ],
   parameters: {
     info: { disable: true },
@@ -171,6 +173,7 @@ export const AsLinkWithAlternateVariant = () => {
         <MenuItem
           href="#"
           onClick={() => {
+            // eslint-disable-next-line no-alert
             alert("clicked");
           }}
           variant="alternate"
@@ -559,3 +562,32 @@ export const IconAlignment = () => {
   );
 };
 IconAlignment.storyName = "Icon & Icon-less Text Alignment";
+
+export const TabbingOrder = () => {
+  return (
+    <Box>
+      <Menu menuType="white">
+        <MenuItem submenu="Menu Item Three">
+          <MenuItem href="#">Item Submenu One</MenuItem>
+          <MenuDivider size="large" />
+          <MenuSegmentTitle text="segment title" variant="alternate">
+            <MenuItem variant="alternate" p="2px 16px">
+              <Search
+                placeholder="Dark variant"
+                variant="dark"
+                defaultValue=""
+              />
+            </MenuItem>
+            <MenuItem variant="alternate" href="#">
+              Item Submenu Two
+            </MenuItem>
+            <MenuItem variant="alternate" href="#">
+              Item Submenu Three
+            </MenuItem>
+          </MenuSegmentTitle>
+        </MenuItem>
+      </Menu>
+    </Box>
+  );
+};
+TabbingOrder.storyName = "Tabbing Order";
diff --git a/src/components/search/search.test.tsx b/src/components/search/search.test.tsx
index fb8777b75..3f6b533f4 100644
--- a/src/components/search/search.test.tsx
+++ b/src/components/search/search.test.tsx
@@ -322,7 +322,7 @@ test("remove button can be reached via keyboard when present", async () => {
   });
   await user.tab();
 
-  expect(screen.getByTestId("input-icon-toggle")).toHaveFocus();
+  expect(screen.getByTestId("input-icon-toggle")).not.toHaveFocus();
 });
 
 test("when a character key is pressed, propagation of the event to parent elements is prevented", async () => {
@@ -428,3 +428,22 @@ test("applies the correct maxWidth specified by the user", () => {
     "max-width": "67%",
   });
 });
+
+test("clears the value when the escape key is pressed", async () => {
+  const user = userEvent.setup();
+  const mockOnChange = jest.fn();
+  render(
+    <Search
+      value="foo"
+      onChange={mockOnChange}
+      name="search-bar"
+      id="search-id"
+    />,
+  );
+
+  const input = screen.getByRole("textbox");
+  await user.click(input);
+  await user.keyboard("{Escape}");
+  expect(screen.queryByText("foo")).not.toBeInTheDocument();
+  expect(mockOnChange).toHaveBeenCalled();
+});