diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67ac75261..59ac914cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,7 @@ Currently, this repo is in Prerelease. When it is released, this project will ad
- Exports `useMediaQuery` and `chakra` from Chakra UI.
- Updates `Accordion` to close focused panel when "esc" key is pressed.
- Updates the `TextInput` and `SearchBar` components to better associate the input element to the entire component's helper text.
+- Updates `Accordion` to close panel when element within panel is focused and "esc" key is pressed.
## 3.1.4 (May 23, 2024)
diff --git a/src/components/Accordion/Accordion.mdx b/src/components/Accordion/Accordion.mdx
index 84c6609b1..9deb30dbb 100644
--- a/src/components/Accordion/Accordion.mdx
+++ b/src/components/Accordion/Accordion.mdx
@@ -118,7 +118,7 @@ const accordionData = [
- The `ariaLabel` key available within the `accordionData` array entries can set
a unique value for each accordion within an accordion group.
- If the user presses the 'esc' key and focus is on an open accordion panel,
- that panel will close.
+ that panel will close and focus will return to the accordion's button.
Resources:
diff --git a/src/components/Accordion/Accordion.test.tsx b/src/components/Accordion/Accordion.test.tsx
index 99473d83d..9e001f6cf 100644
--- a/src/components/Accordion/Accordion.test.tsx
+++ b/src/components/Accordion/Accordion.test.tsx
@@ -83,8 +83,13 @@ export const accordionData = [
label: "Tom Nook",
panel: (
- Tom Nook, known in Japan as Tanukichi, is a fictional character
- in the Animal Crossing series who operates the village store.
+ Tom Nook,
+
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ known in Japan as Tanukichi
+
+ , is a fictional character in the Animal Crossing series who operates
+ the village store.
),
},
@@ -146,7 +151,7 @@ describe("Accordion", () => {
const accordionLabel = screen.getByRole("button", { name: "Tom Nook" });
let accordionPanelContent = screen.queryByText(
- /known in Japan as Tanukichi/i
+ /operates the village store/i
);
expect(accordionLabel).toHaveAttribute("aria-expanded", "false");
// The panel's content should not be in the DOM unless the Accordion is open.
@@ -154,25 +159,39 @@ describe("Accordion", () => {
userEvent.click(accordionLabel);
- accordionPanelContent = screen.queryByText(/known in Japan as Tanukichi/i);
+ accordionPanelContent = screen.queryByText(/operates the village store/i);
expect(accordionLabel).toHaveAttribute("aria-expanded", "true");
expect(accordionPanelContent).toBeInTheDocument();
});
- it("closes the accordion when the 'esc' key is pressed", async () => {
- render();
+ it("closes the accordion when the button is in focus and the 'esc' key is pressed", async () => {
+ render();
- const accordionLabel = screen.getByRole("button", { name: "Tom Nook" });
- let accordionPanelContent = screen.queryByText(
- /known in Japan as Tanukichi/i
- );
- expect(accordionLabel).toHaveAttribute("aria-expanded", "true");
- expect(accordionPanelContent).toBeInTheDocument();
+ const accordionButton = screen.getByRole("button");
- await userEvent.type(accordionLabel, "[Escape]");
+ await userEvent.click(accordionButton);
- expect(accordionLabel).toHaveAttribute("aria-expanded", "false");
- expect(accordionPanelContent).not.toBeInTheDocument();
+ expect(accordionButton.getAttribute("aria-expanded")).toEqual("true");
+
+ await userEvent.keyboard("[Escape]");
+
+ expect(accordionButton).toHaveAttribute("aria-expanded", "false");
+ });
+
+ it("closes the accordion when an element in the panel is focused and the 'esc' key is pressed", async () => {
+ render();
+
+ const accordionButton = screen.getByRole("button");
+
+ await userEvent.click(accordionButton);
+
+ expect(accordionButton.getAttribute("aria-expanded")).toEqual("true");
+
+ const linkInPanel = screen.getByRole("link");
+
+ await userEvent.type(linkInPanel, "[Escape]");
+
+ expect(accordionButton).toHaveAttribute("aria-expanded", "false");
});
it("always renders its content when isAlwaysRendered is true", () => {
@@ -180,7 +199,7 @@ describe("Accordion", () => {
const accordionLabel = screen.getByRole("button", { name: "Tom Nook" });
let accordionPanelContent = screen.queryByText(
- /known in Japan as Tanukichi/i
+ /operates the village store/i
);
expect(accordionLabel).toHaveAttribute("aria-expanded", "false");
expect(accordionPanelContent).toBeInTheDocument();
@@ -226,6 +245,29 @@ describe("Accordion", () => {
expect(accordion3).toHaveAttribute("aria-expanded", "true");
});
+ it("closes only the focused accordion when there are multiple accordions and the 'esc' key is pressed", async () => {
+ render();
+
+ const accordion1 = screen.getByRole("button", { name: "Tom Nook" });
+ const accordion3 = screen.getByRole("button", { name: "K.K. Slider" });
+
+ expect(accordion1).toHaveAttribute("aria-expanded", "false");
+ expect(accordion3).toHaveAttribute("aria-expanded", "false");
+
+ userEvent.click(accordion1);
+ userEvent.click(accordion3);
+
+ expect(accordion1).toHaveAttribute("aria-expanded", "true");
+ expect(accordion3).toHaveAttribute("aria-expanded", "true");
+
+ const linkInPanel1 = screen.getByRole("link");
+
+ userEvent.type(linkInPanel1, "[Escape]");
+
+ expect(accordion1).toHaveAttribute("aria-expanded", "false");
+ expect(accordion3).toHaveAttribute("aria-expanded", "true");
+ });
+
describe("Check aria-label values", () => {
it("renders with attribute generated from component prop", () => {
render(
diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx
index 15089afc6..39f2267fb 100644
--- a/src/components/Accordion/Accordion.tsx
+++ b/src/components/Accordion/Accordion.tsx
@@ -224,16 +224,41 @@ export const Accordion: ChakraComponent<
isDefaultOpen ? [0] : []
);
+ // If the accordionData doesn't already contain refs for the panel
+ // buttons, add them now.
+ const updatedAccordionData = accordionData.map((item) => ({
+ ...item,
+ buttonInteractionRef: item.buttonInteractionRef || React.createRef(),
+ }));
+
const handleKeyDown = (e) => {
- // If the 'esc' key is pressed,
- if (e.keyCode === 27) {
- const focusedPanelIndex = Number(e.target.dataset.index);
- // collapse the currently focused panel.
- // (Nothing will happen if the currently
- // focused panel is already collapsed.)
+ // If the 'esc' key is pressed, find the panel the
+ // user is focused on or within, and remove it as
+ // an expanded panel. (Nothing will happen if the
+ // panel is already collapsed.)
+ if (e.code === "Escape") {
+ let focusedPanelIndex;
+ if (e.target.dataset.index) {
+ // If the user is focused on an accordion button...
+ focusedPanelIndex = Number(e.target.dataset.index);
+ } else {
+ // If the user is focused on an element within the panel...
+ focusedPanelIndex = Number(
+ e.target.closest("[role='region']").id.split("-").pop()
+ );
+ }
+
setExpandedPanels(
expandedPanels.filter((i) => i !== focusedPanelIndex)
);
+
+ // If something *inside* the accordion was in focus and 'esc' was clicked,
+ // return focus to the accordion panel
+ if (updatedAccordionData[focusedPanelIndex].buttonInteractionRef) {
+ updatedAccordionData[
+ focusedPanelIndex
+ ].buttonInteractionRef.current.focus();
+ }
}
};
@@ -248,7 +273,7 @@ export const Accordion: ChakraComponent<
{...rest}
>
{getElementsFromData(
- accordionData,
+ updatedAccordionData,
ariaLabel,
id,
isAlwaysRendered,
diff --git a/src/components/Accordion/accordionChangelogData.ts b/src/components/Accordion/accordionChangelogData.ts
index a3e6dbe7f..2cc54e80d 100644
--- a/src/components/Accordion/accordionChangelogData.ts
+++ b/src/components/Accordion/accordionChangelogData.ts
@@ -14,7 +14,10 @@ export const changelogData: ChangelogData[] = [
version: "3.1.4",
type: "Update",
affects: ["Accessibility", "Functionality"],
- notes: ["Adds logic to close accordion when 'esc' key is pressed"],
+ notes: [
+ "Adds logic to close accordion when accordion button is focused and 'esc' key is pressed",
+ "Adds logic to close accordion when element within panel is focused and 'esc' key is pressed",
+ ],
},
{
date: "2024-03-14",
diff --git a/src/components/MultiSelect/MultiSelect.mdx b/src/components/MultiSelect/MultiSelect.mdx
index 502d334d4..e8e995f0d 100644
--- a/src/components/MultiSelect/MultiSelect.mdx
+++ b/src/components/MultiSelect/MultiSelect.mdx
@@ -95,6 +95,9 @@ Accordion:
- Use the `aria-controls` attribute to associate the control with the panel.
- The open and close icons are decorative (`aria-hidden` is `true`).
- Visible focus goes around full button and full button is clickable.
+- Because the `MultiSelect` utilizes the `Accordion`, the same functionality applies when
+ the user presses the 'esc' key and focus is on an open panel: that panel will close and
+ focus will return to the panel's button.
Checkbox:
diff --git a/src/components/MultiSelect/MultiSelect.test.tsx b/src/components/MultiSelect/MultiSelect.test.tsx
index 3d458602f..088e7a027 100644
--- a/src/components/MultiSelect/MultiSelect.test.tsx
+++ b/src/components/MultiSelect/MultiSelect.test.tsx
@@ -263,6 +263,34 @@ describe.skip("MultiSelect", () => {
).not.toBeInTheDocument();
});
+ it("closes the multiselect when the 'esc' key is pressed", async () => {
+ render(
+ null}
+ onClear={() => null}
+ />
+ );
+
+ const multiSelectButton = screen.getByRole("button");
+ const multiSelectCheckbox = screen.getByRole("checkbox", { name: /dogs/i });
+
+ expect(multiSelectButton.getAttribute("aria-expanded")).toEqual("true");
+
+ await userEvent.click(multiSelectCheckbox);
+
+ await userEvent.keyboard("[Escape]");
+
+ expect(multiSelectButton).toHaveAttribute("aria-expanded", "false");
+ });
+
it("should allow user to toggle menu by clicking menu button or use the 'Enter'/'Spacebar' key", () => {
render(
=
+ const accordionButtonRef: React.RefObject =
useRef();
const expandToggleButtonRef: React.RefObject =
useRef();
@@ -364,7 +364,7 @@ export const MultiSelect: ChakraComponent<
{
accordionType: "default",
// Pass the ref for interaction with the accordion button.
- buttonInteractionRef: accordianButtonRef,
+ buttonInteractionRef: accordionButtonRef,
label: accordionLabel,
panel: accordionPanel,
},
@@ -387,7 +387,7 @@ export const MultiSelect: ChakraComponent<
selectedItemsString={selectedItemsString}
selectedItemsCount={selectedItemsCount}
onClear={onClear}
- accordianButtonRef={accordianButtonRef}
+ accordionButtonRef={accordionButtonRef}
/>
)}
diff --git a/src/components/MultiSelect/MultiSelectItemsCountButton.tsx b/src/components/MultiSelect/MultiSelectItemsCountButton.tsx
index 2e13dfda2..782a9135f 100644
--- a/src/components/MultiSelect/MultiSelectItemsCountButton.tsx
+++ b/src/components/MultiSelect/MultiSelectItemsCountButton.tsx
@@ -24,7 +24,7 @@ export interface MultiSelectItemsCountButtonProps {
/** The action to perform for key down event. */
onKeyDown?: () => void;
/** Ref to the Accordion Button element. */
- accordianButtonRef: any;
+ accordionButtonRef: any;
}
/**
@@ -42,7 +42,7 @@ const MultiSelectItemsCountButton = forwardRef<
isOpen,
multiSelectId,
multiSelectLabelText,
- accordianButtonRef,
+ accordionButtonRef,
onClear,
selectedItemsString,
selectedItemsCount,
@@ -66,7 +66,7 @@ const MultiSelectItemsCountButton = forwardRef<
onClear && onClear();
// Set focus on the Accordion Button when close the
// selected items count button.
- accordianButtonRef.current?.focus();
+ accordionButtonRef.current?.focus();
}}
__css={styles}
>