Repository: Sage/carbon (ts)

Issue:
Press escape to dismiss dropdown on sidebar
### Current behaviour

This is a minor accessibility issue
While navigating in a sidebar, once pressing the escape key to close a drop down list the whole modal is closed instead.

### Expected behaviour

It should close the current dropdown open and not the complete sidebar

### CodeSandbox or Storybook URL

https://codesandbox.io/s/sidebar-with-select-nllhe4

### JIRA Ticket (Sage Only)

_No response_

### Suggested Solution

_No response_

### Carbon Version

111.8.0

### Design Tokens Version

2.45.0

### 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:
fix(select): resolve issue with Select component closing parent Dialog on escape key press - FE-5624
### Proposed behaviour

Fixes #5716


### 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] All themes are supported if required
- [x] Unit tests added or updated if required
- [x] Cypress automation tests added or updated if required
- [x] Storybook added or updated if required
- [x] Translations added or updated (including creating or amending translation keys table in storybook) if required
- [x] Typescript `d.ts` file added or updated if required
- [x] Related docs have been 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

<!-- 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 -->


Patch:
diff --git a/src/components/select/multi-select/multi-select.component.js b/src/components/select/multi-select/multi-select.component.js
index 1b8d12bca..239e124e5 100644
--- a/src/components/select/multi-select/multi-select.component.js
+++ b/src/components/select/multi-select/multi-select.component.js
@@ -465,10 +465,10 @@ const MultiSelect = React.forwardRef(
       [textboxRef, actualValue, updateValue]
     );
 
-    function onSelectListClose() {
+    const onSelectListClose = useCallback(() => {
       setOpenState(false);
       setFilterText("");
-    }
+    }, []);
 
     function findElementWithMatchingText(textToMatch, list) {
       return list.find((child) => {
diff --git a/src/components/select/select-list/select-list.component.js b/src/components/select/select-list/select-list.component.js
index 7d3dbfec3..1d7808aee 100644
--- a/src/components/select/select-list/select-list.component.js
+++ b/src/components/select/select-list/select-list.component.js
@@ -12,6 +12,7 @@ import { useVirtualizer } from "@tanstack/react-virtual";
 import findLastIndex from "lodash/findLastIndex";
 
 import useScrollBlock from "../../../hooks/__internal__/useScrollBlock";
+import useModalManager from "../../../hooks/__internal__/useModalManager";
 import {
   StyledSelectList,
   StyledSelectLoaderContainer,
@@ -292,7 +293,7 @@ const SelectList = React.forwardRef(
 
         if (key === "Tab" && listActionButton) {
           handleActionButtonTab(event, isActionButtonFocused);
-        } else if (key === "Tab" || key === "Escape") {
+        } else if (key === "Tab") {
           onSelectListClose();
         } else if (key === "Enter" && !isActionButtonFocused) {
           event.preventDefault();
@@ -331,6 +332,17 @@ const SelectList = React.forwardRef(
       ]
     );
 
+    const handleEscapeKey = useCallback(
+      (event) => {
+        if (event.key === "Escape") {
+          onSelectListClose();
+        }
+      },
+      [onSelectListClose]
+    );
+
+    useModalManager(isOpen, handleEscapeKey, listRef);
+
     const handleListScroll = useCallback(
       (event) => {
         const element = event.target;
diff --git a/src/components/select/select-list/select-list.spec.js b/src/components/select/select-list/select-list.spec.js
index 54df0cbb8..d2fbe20c4 100644
--- a/src/components/select/select-list/select-list.spec.js
+++ b/src/components/select/select-list/select-list.spec.js
@@ -23,7 +23,7 @@ const mockedGuid = "guid-12345";
 const guidSpy = jest.spyOn(guidModule, "default");
 guidSpy.mockImplementation(() => "guid-12345");
 
-const escapeKeyDownEvent = new KeyboardEvent("keydown", {
+const escapeKeyUpEvent = new KeyboardEvent("keyup", {
   key: "Escape",
   bubbles: true,
 });
@@ -51,6 +51,10 @@ const endKeyDownEvent = new KeyboardEvent("keydown", {
   key: "End",
   bubbles: true,
 });
+const spaceKeyUpEvent = new KeyboardEvent("keyup", {
+  key: "Space",
+  bubbles: true,
+});
 
 describe("SelectList", () => {
   describe.each([getSelectList, getOptionRowSelectList])(
@@ -86,7 +90,7 @@ describe("SelectList", () => {
       });
 
       describe.each([
-        ["Escape", escapeKeyDownEvent],
+        ["Escape", escapeKeyUpEvent],
         ["Tab", tabKeyDownEvent],
         ["Enter", enterKeyDownEvent],
       ])("and it's the %s key", (_, keyEvent) => {
@@ -96,6 +100,13 @@ describe("SelectList", () => {
         });
       });
 
+      describe("and it's a Space key", () => {
+        it("then the onSelectListClose prop should not be called", () => {
+          testContainer.dispatchEvent(spaceKeyUpEvent);
+          expect(onSelectListCloseFn).not.toHaveBeenCalled();
+        });
+      });
+
       describe("and it's the Enter key", () => {
         it("then should call a preventDefault function", () => {
           const spy = jest.spyOn(enterKeyDownEvent, "preventDefault");
diff --git a/src/components/select/simple-select/simple-select.component.js b/src/components/select/simple-select/simple-select.component.js
index 989ed280c..ee8030b16 100644
--- a/src/components/select/simple-select/simple-select.component.js
+++ b/src/components/select/simple-select/simple-select.component.js
@@ -362,9 +362,9 @@ const SimpleSelect = React.forwardRef(
       }
     }
 
-    function onSelectListClose() {
+    const onSelectListClose = useCallback(() => {
       setOpenState(false);
-    }
+    }, []);
 
     const assignInput = useCallback(
       (element) => {


Test patch:
diff --git a/src/components/select/filterable-select/filterable-select.test.js b/src/components/select/filterable-select/filterable-select.test.js
index 26b5ec2df..2152b59a1 100644
--- a/src/components/select/filterable-select/filterable-select.test.js
+++ b/src/components/select/filterable-select/filterable-select.test.js
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
 import FilterableSelect from "./filterable-select.component";
 import Option from "../option/option.component";
 import OptionRow from "../option-row/option-row.component";
@@ -15,6 +15,7 @@ import {
 } from "../../../../cypress/locators";
 
 import {
+  selectList,
   selectListWrapper,
   selectOption,
   selectOptionByText,
@@ -38,6 +39,8 @@ import {
 
 import { loader } from "../../../../cypress/locators/loader";
 
+import { alertDialogPreview } from "../../../../cypress/locators/dialog";
+
 import {
   useJQueryCssValueAndAssert,
   verifyRequiredAsteriskForLabel,
@@ -45,8 +48,6 @@ import {
 
 import { keyCode, positionOfElement } from "../../../../cypress/support/helper";
 
-import { alertDialogPreview } from "../../../../cypress/locators/dialog";
-
 import {
   SIZE,
   CHARACTERS,
@@ -597,6 +598,20 @@ const FilterableSelectWithManyOptionsAndVirtualScrolling = () => (
   </FilterableSelect>
 );
 
+const FilterableSelectNestedInDialog = () => {
+  const [isOpen, setIsOpen] = useState(true);
+  return (
+    <Dialog open={isOpen} onCancel={() => setIsOpen(false)} title="Dialog">
+      <FilterableSelect name="testSelect" id="testSelect">
+        <Option value="opt1" text="red" />
+        <Option value="opt2" text="green" />
+        <Option value="opt3" text="blue" />
+        <Option value="opt4" text="black" />
+      </FilterableSelect>
+    </Dialog>
+  );
+};
+
 const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS];
 const testPropValue = CHARACTERS.STANDARD;
 const addElementText = "Add a New Element";
@@ -913,7 +928,7 @@ context("Tests for Filterable Select component", () => {
 
       dropdownButton().click();
       selectListWrapper().should("be.visible");
-      selectInput().trigger("keydown", { ...keyCode("Esc") });
+      selectInput().type("{esc}", { force: true });
       selectInput().should("have.attr", "aria-expanded", "false");
       selectListWrapper().should("not.be.visible");
     });
@@ -1536,4 +1551,24 @@ context("Tests for Filterable Select component", () => {
       selectOptionByText("Option 1002.").should("be.visible");
     });
   });
+
+  describe("when nested inside of a Dialog component", () => {
+    it("should not close the Dialog when Select is closed by pressing an escape key", () => {
+      CypressMountWithProviders(<FilterableSelectNestedInDialog />);
+
+      dropdownButton().click();
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          selectList().should("not.be.visible");
+          alertDialogPreview().should("exist");
+        });
+
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          alertDialogPreview().should("not.exist");
+        });
+    });
+  });
 });
diff --git a/src/components/select/multi-select/multi-select.test.js b/src/components/select/multi-select/multi-select.test.js
index c636c2715..9fcd35ca2 100644
--- a/src/components/select/multi-select/multi-select.test.js
+++ b/src/components/select/multi-select/multi-select.test.js
@@ -1,9 +1,10 @@
-import React from "react";
+import React, { useState } from "react";
 import MultiSelect from "./multi-select.component";
 import Option from "../option/option.component";
 import OptionRow from "../option-row/option-row.component";
 import Button from "../../button/button.component";
 import CypressMountWithProviders from "../../../../cypress/support/component-helper/cypress-mount";
+import Dialog from "../../dialog";
 
 import {
   getDataElementByValue,
@@ -14,6 +15,7 @@ import {
 } from "../../../../cypress/locators";
 
 import {
+  selectList,
   selectListWrapper,
   selectOption,
   selectOptionByText,
@@ -39,6 +41,8 @@ import { pillCloseIcon } from "../../../../cypress/locators/pill";
 
 import { loader } from "../../../../cypress/locators/loader";
 
+import { alertDialogPreview } from "../../../../cypress/locators/dialog";
+
 import {
   useJQueryCssValueAndAssert,
   verifyRequiredAsteriskForLabel,
@@ -488,6 +492,20 @@ const MultiSelectWithManyOptionsAndVirtualScrolling = () => (
   </MultiSelect>
 );
 
+const MultiSelectNestedInDialog = () => {
+  const [isOpen, setIsOpen] = useState(true);
+  return (
+    <Dialog open={isOpen} onCancel={() => setIsOpen(false)} title="Dialog">
+      <MultiSelect name="testSelect" id="testSelect">
+        <Option value="opt1" text="red" />
+        <Option value="opt2" text="green" />
+        <Option value="opt3" text="blue" />
+        <Option value="opt4" text="black" />
+      </MultiSelect>
+    </Dialog>
+  );
+};
+
 const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS];
 const testPropValue = CHARACTERS.STANDARD;
 const columns = 3;
@@ -1371,4 +1389,24 @@ context("Tests for Multi Select component", () => {
       selectOptionByText("Option 1002.").should("be.visible");
     });
   });
+
+  describe("when nested inside of a Dialog component", () => {
+    it("should not close the Dialog when Select is closed by pressing an escape key", () => {
+      CypressMountWithProviders(<MultiSelectNestedInDialog />);
+
+      dropdownButton().click();
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          selectList().should("not.be.visible");
+          alertDialogPreview().should("exist");
+        });
+
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          alertDialogPreview().should("not.exist");
+        });
+    });
+  });
 });
diff --git a/src/components/select/simple-select/simple-select.test.js b/src/components/select/simple-select/simple-select.test.js
index fa7d949e2..6757f4f36 100644
--- a/src/components/select/simple-select/simple-select.test.js
+++ b/src/components/select/simple-select/simple-select.test.js
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
 import SimpleSelect from "./simple-select.component";
 import Option from "../option/option.component";
 import OptionRow from "../option-row/option-row.component";
@@ -6,6 +6,7 @@ import OptionGroupHeader from "../option-group-header/option-group-header.compon
 import Icon from "../../icon/icon.component";
 import Box from "../../box";
 import CypressMountWithProviders from "../../../../cypress/support/component-helper/cypress-mount";
+import Dialog from "../../dialog";
 
 import {
   getDataElementByValue,
@@ -33,7 +34,9 @@ import {
   selectDataComponent,
   selectElementInput,
   multiColumnsSelectListRowAt,
+  selectList,
 } from "../../../../cypress/locators/select";
+import { alertDialogPreview } from "../../../../cypress/locators/dialog";
 
 import { loader } from "../../../../cypress/locators/loader";
 
@@ -495,6 +498,20 @@ const SimpleSelectWithManyOptionsAndVirtualScrolling = () => (
   </SimpleSelect>
 );
 
+const SimpleSelectNestedInDialog = () => {
+  const [isOpen, setIsOpen] = useState(true);
+  return (
+    <Dialog open={isOpen} onCancel={() => setIsOpen(false)} title="Dialog">
+      <SimpleSelect name="testSelect" id="testSelect">
+        <Option value="opt1" text="red" />
+        <Option value="opt2" text="green" />
+        <Option value="opt3" text="blue" />
+        <Option value="opt4" text="black" />
+      </SimpleSelect>
+    </Dialog>
+  );
+};
+
 context("Tests for Simple Select component", () => {
   describe("check props for Simple Select component", () => {
     it.each(testData)(
@@ -753,7 +770,7 @@ context("Tests for Simple Select component", () => {
 
       selectText().click();
       selectListWrapper().should("be.visible");
-      selectText().trigger("keydown", { ...keyCode("Esc") });
+      selectText().type("{esc}", { force: true });
       selectInput().should("have.attr", "aria-expanded", "false");
       selectListWrapper().should("not.be.visible");
     });
@@ -1221,4 +1238,24 @@ context("Tests for Simple Select component", () => {
       selectOptionByText("Option 40.").should("not.exist");
     });
   });
+
+  describe("when nested inside of a Dialog component", () => {
+    it("should not close the Dialog when Select is closed by pressing an escape key", () => {
+      CypressMountWithProviders(<SimpleSelectNestedInDialog />);
+
+      selectText().click();
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          selectList().should("not.be.visible");
+          alertDialogPreview().should("exist");
+        });
+
+      commonDataElementInputPreview()
+        .type("{esc}", { force: true })
+        .then(() => {
+          alertDialogPreview().should("not.exist");
+        });
+    });
+  });
 });