diff --git a/packages/wow-ui/src/components/DropDown/DropDown.test.tsx b/packages/wow-ui/src/components/DropDown/DropDown.test.tsx
new file mode 100644
index 00000000..27ad15e9
--- /dev/null
+++ b/packages/wow-ui/src/components/DropDown/DropDown.test.tsx
@@ -0,0 +1,203 @@
+"use client";
+
+import { render, screen, waitFor } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import type { ReactNode } from "react";
+import { useState } from "react";
+
+import DropDown from "@/components/DropDown";
+import DropDownOption from "@/components/DropDown/DropDownOption";
+
+const dropdownId = "dropdown-id";
+
+const options = [
+ { value: "option1", text: "Option 1" },
+ { value: "option2", text: "Option 2" },
+ { value: "option3", text: "Option 3" },
+];
+
+const renderDropDown = (props = {}) =>
+ render(
+
+ {options.map((option) => (
+
+ ))}
+
+ );
+
+describe("DropDown component", () => {
+ it("should render the placeholder", () => {
+ renderDropDown({ placeholder: "Please select" });
+ expect(screen.getByText("Please select")).toBeInTheDocument();
+ });
+
+ it("should allow selection of an option", async () => {
+ const dropdownId = "dropdown-id";
+ renderDropDown({ id: dropdownId, placeholder: "Please select" });
+
+ await userEvent.click(screen.getByText("Please select"));
+ await userEvent.click(screen.getByText("Option 1"));
+
+ await waitFor(() => {
+ const option1 = document.querySelector(`#${dropdownId}-option-option1`);
+ expect(option1).toHaveAttribute("aria-selected", "true");
+ });
+ });
+
+ it("should render with default value", () => {
+ renderDropDown({
+ defaultValue: "option2",
+ placeholder: "Please select",
+ });
+
+ const dropdownTrigger = screen.getByRole("combobox");
+ expect(dropdownTrigger).toHaveTextContent("Option 2");
+ });
+
+ it("should render the trigger button", async () => {
+ renderDropDown({ trigger: });
+
+ userEvent.click(screen.getByText("Open Dropdown"));
+
+ await waitFor(() => {
+ expect(screen.getByText("Option 1")).toBeInTheDocument();
+ });
+ });
+
+ it("closes dropdown when clicking outside", async () => {
+ renderDropDown({ placeholder: "Select an option" });
+ await userEvent.click(screen.getByText("Select an option"));
+ await userEvent.click(document.body);
+
+ await waitFor(() => {
+ const dropdown = screen.queryByRole("listbox");
+ expect(dropdown).toBeNull();
+ });
+ });
+});
+
+describe("external control and events", () => {
+ it("should fire external onChange event and update controlled state", async () => {
+ const ControlledDropDown = () => {
+ const [selectedValue, setSelectedValue] = useState("");
+
+ const handleChange = (value: {
+ selectedValue: string;
+ selectedText: ReactNode;
+ }) => {
+ setSelectedValue(value.selectedValue);
+ };
+
+ return (
+
+ {options.map((option) => (
+
+ ))}
+
+ );
+ };
+
+ render();
+
+ await userEvent.click(screen.getByText("Please select"));
+ await userEvent.click(screen.getByText("Option 2"));
+
+ await waitFor(() => {
+ const option2 = document.querySelector(`#${dropdownId}-option-option2`);
+ expect(option2).toHaveAttribute("aria-selected", "true");
+ });
+ });
+
+ it("should navigate options using keyboard and apply focus correctly", async () => {
+ const ControlledDropDown = () => {
+ const [selectedValue, setSelectedValue] = useState("");
+
+ const handleChange = (value: {
+ selectedValue: string;
+ selectedText: ReactNode;
+ }) => {
+ setSelectedValue(value.selectedValue);
+ };
+
+ return (
+
+ {options.map((option) => (
+
+ ))}
+
+ );
+ };
+
+ render();
+ await userEvent.click(screen.getByText("Please select"));
+
+ await userEvent.keyboard("{ArrowDown}");
+ await waitFor(() => {
+ const dropdownTrigger = screen.getByRole("combobox");
+ expect(dropdownTrigger).toHaveAttribute(
+ "aria-activedescendant",
+ `${dropdownId}-option-option1`
+ );
+ });
+
+ await userEvent.keyboard("{ArrowDown}");
+ await waitFor(() => {
+ const dropdownTrigger = screen.getByRole("combobox");
+ expect(dropdownTrigger).toHaveAttribute(
+ "aria-activedescendant",
+ `${dropdownId}-option-option2`
+ );
+ });
+
+ await userEvent.keyboard("{ArrowDown}");
+ await waitFor(() => {
+ const dropdownTrigger = screen.getByRole("combobox");
+ expect(dropdownTrigger).toHaveAttribute(
+ "aria-activedescendant",
+ `${dropdownId}-option-option3`
+ );
+ });
+
+ await userEvent.keyboard("{ArrowUp}");
+ await waitFor(() => {
+ const dropdownTrigger = screen.getByRole("combobox");
+ expect(dropdownTrigger).toHaveAttribute(
+ "aria-activedescendant",
+ `${dropdownId}-option-option2`
+ );
+ });
+
+ await userEvent.keyboard("{Enter}");
+ await waitFor(() => {
+ const option2 = document.querySelector(`#${dropdownId}-option-option2`);
+ expect(option2).toHaveAttribute("aria-selected", "true");
+
+ const option1 = document.querySelector(`#${dropdownId}-option-option1`);
+ const option3 = document.querySelector(`#${dropdownId}-option-option3`);
+ expect(option1).toHaveAttribute("aria-selected", "false");
+ expect(option3).toHaveAttribute("aria-selected", "false");
+ });
+ });
+});
diff --git a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
index fda4348e..7fd348fb 100644
--- a/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
+++ b/packages/wow-ui/src/components/DropDown/DropDownOption.tsx
@@ -52,6 +52,7 @@ const DropDownOption = forwardRef(
return (
{
- const { open, setFocusedValue, focusedValue, handleSelect } =
+ const { open, setFocusedValue, focusedValue, handleSelect, dropdownId } =
useDropDownContext();
const itemMap = useCollection();
const listRef = useRef(null);
@@ -64,11 +64,11 @@ export const DropDownOptionList = ({
return (
{
const itemMap = useCollection();
- const { open, selectedValue, setOpen, setFocusedValue, dropdownId } =
- useDropDownContext();
+ const {
+ open,
+ selectedValue,
+ focusedValue,
+ setOpen,
+ setFocusedValue,
+ dropdownId,
+ } = useDropDownContext();
const selectedText = itemMap.get(selectedValue);
@@ -42,13 +48,28 @@ const DropDownTrigger = ({
[toggleDropdown]
);
+ const commonProps: ButtonHTMLAttributes = {
+ "aria-expanded": open,
+ role: "combobox",
+ "aria-haspopup": "listbox",
+ id: `${dropdownId}-trigger`,
+ "aria-controls": `${dropdownId}-option-list`,
+ ...(focusedValue && {
+ "aria-activedescendant": `${dropdownId}-option-${focusedValue}`,
+ }),
+ ...(label
+ ? {
+ "aria-labelledby": `${dropdownId}-label`,
+ }
+ : {
+ "aria-label": `dropdown-open`,
+ }),
+ };
+
if (trigger) {
return cloneElement(trigger, {
onClick: toggleDropdown,
- "aria-expanded": open,
- "aria-haspopup": "true",
- id: `${dropdownId}-trigger`,
- "aria-controls": `${dropdownId}`,
+ ...commonProps,
});
}
@@ -57,19 +78,17 @@ const DropDownTrigger = ({
{label && (
{label}
)}
);
};
+
+export default DropDownWrapper;
diff --git a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts
index 7757ad7f..b0e16dc4 100644
--- a/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts
+++ b/packages/wow-ui/src/components/DropDown/context/DropDownContext.ts
@@ -1,7 +1,7 @@
import { createContext } from "react";
-import type useDropDownState from "../../../hooks/useDropDownState";
-import useSafeContext from "../../../hooks/useSafeContext";
+import type useDropDownState from "@/hooks/useDropDownState";
+import useSafeContext from "@/hooks/useSafeContext";
export const DropDownContext = createContext<
(ReturnType & { dropdownId: string }) | null
diff --git a/packages/wow-ui/src/components/DropDown/index.tsx b/packages/wow-ui/src/components/DropDown/index.tsx
index 47cf5803..60f72a80 100644
--- a/packages/wow-ui/src/components/DropDown/index.tsx
+++ b/packages/wow-ui/src/components/DropDown/index.tsx
@@ -9,12 +9,12 @@ import type {
import { useId } from "react";
import { DropDownContext } from "@/components/DropDown/context/DropDownContext";
-import { DropDownOptionList } from "@/components/DropDown/DropDownOptionList";
+import DropDownOptionList from "@/components/DropDown/DropDownOptionList";
import DropDownTrigger from "@/components/DropDown/DropDownTrigger";
+import DropDownWrapper from "@/components/DropDown/DropDownWrapper";
import useDropDownState from "@/hooks/useDropDownState";
import { CollectionProvider } from "./context/CollectionContext";
-import { DropDownWrapper } from "./DropDownWrapper";
export interface DropDownWithTriggerProps extends PropsWithChildren {
/**
* @description 드롭다운을 열기 위한 외부 트리거 요소입니다.