Skip to content

Commit

Permalink
Merge pull request #1607 from NYPL/DSD-1740/close-dropdown-multiselect
Browse files Browse the repository at this point in the history
DSD-1740/close dropdown multiselect
  • Loading branch information
oliviawongnyc authored Jun 25, 2024
2 parents 75340ac + 0bc9a58 commit bae5120
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/components/Accordion/Accordion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
74 changes: 58 additions & 16 deletions src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ export const accordionData = [
label: "Tom Nook",
panel: (
<p>
Tom Nook, <b>known in Japan as Tanukichi</b>, is a fictional character
in the Animal Crossing series who operates the village store.
Tom Nook,
<b>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
known in <a href="#">Japan</a> as Tanukichi
</b>
, is a fictional character in the Animal Crossing series who operates
the village store.
</p>
),
},
Expand Down Expand Up @@ -146,41 +151,55 @@ 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.
expect(accordionPanelContent).not.toBeInTheDocument();

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(<Accordion accordionData={[accordionData[0]]} isDefaultOpen />);
it("closes the accordion when the button is in focus and the 'esc' key is pressed", async () => {
render(<Accordion accordionData={[accordionData[0]]} />);

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(<Accordion accordionData={[accordionData[0]]} />);

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", () => {
render(<Accordion accordionData={[accordionData[0]]} isAlwaysRendered />);

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();
Expand Down Expand Up @@ -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(<Accordion accordionData={accordionData} />);

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(
Expand Down
39 changes: 32 additions & 7 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
};

Expand All @@ -248,7 +273,7 @@ export const Accordion: ChakraComponent<
{...rest}
>
{getElementsFromData(
accordionData,
updatedAccordionData,
ariaLabel,
id,
isAlwaysRendered,
Expand Down
5 changes: 4 additions & 1 deletion src/components/Accordion/accordionChangelogData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/components/MultiSelect/MultiSelect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
28 changes: 28 additions & 0 deletions src/components/MultiSelect/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,34 @@ describe.skip("MultiSelect", () => {
).not.toBeInTheDocument();
});

it("closes the multiselect when the 'esc' key is pressed", async () => {
render(
<MultiSelect
id="multiselect-test-id"
buttonText="Multiselect button text"
defaultItemsVisible={defaultItemsVisible}
items={items}
isDefaultOpen={true}
isSearchable={false}
isBlockElement={false}
selectedItems={selectedTestItems}
onChange={() => 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(
<MultiSelect
Expand Down
8 changes: 4 additions & 4 deletions src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChakraComponent,
useMultiStyleConfig,
} from "@chakra-ui/react";
import React, { useState, forwardRef, useRef } from "react";
import React, { forwardRef, useRef, useState } from "react";

import Accordion from "./../Accordion/Accordion";
import Button from "./../Button/Button";
Expand Down Expand Up @@ -95,7 +95,7 @@ export const MultiSelect: ChakraComponent<

// Create a ref to hold a reference to the accordian button, enabling us
// to programmatically focus it.
const accordianButtonRef: React.RefObject<HTMLDivElement> =
const accordionButtonRef: React.RefObject<HTMLDivElement> =
useRef<HTMLDivElement>();
const expandToggleButtonRef: React.RefObject<HTMLButtonElement> =
useRef<HTMLButtonElement>();
Expand Down Expand Up @@ -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,
},
Expand All @@ -387,7 +387,7 @@ export const MultiSelect: ChakraComponent<
selectedItemsString={selectedItemsString}
selectedItemsCount={selectedItemsCount}
onClear={onClear}
accordianButtonRef={accordianButtonRef}
accordionButtonRef={accordionButtonRef}
/>
)}
</Box>
Expand Down
6 changes: 3 additions & 3 deletions src/components/MultiSelect/MultiSelectItemsCountButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -42,7 +42,7 @@ const MultiSelectItemsCountButton = forwardRef<
isOpen,
multiSelectId,
multiSelectLabelText,
accordianButtonRef,
accordionButtonRef,
onClear,
selectedItemsString,
selectedItemsCount,
Expand All @@ -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}
>
Expand Down

0 comments on commit bae5120

Please sign in to comment.