Skip to content

[Feat] #54 - Dropdowns 컴포넌트 구현#60

Merged
kwqnyewest merged 4 commits into
devfrom
feat/#54-dropdowns
Mar 19, 2026
Merged

[Feat] #54 - Dropdowns 컴포넌트 구현#60
kwqnyewest merged 4 commits into
devfrom
feat/#54-dropdowns

Conversation

@kwqnyewest
Copy link
Copy Markdown
Collaborator

@kwqnyewest kwqnyewest commented Mar 16, 2026

🔎 What is this PR?

  • DropdownMenuSelectDropdown 두 가지 공통 드롭다운 컴포넌트 구현

📝 Changes

  • DropdownMenu 컴포넌트 구현 (좌측 아이콘, 단축키, Divider, 비활성화 옵션 지원)
  • SelectDropdown 컴포넌트 구현 (선택값 표시, placeholder, 전체 너비, 비활성화 지원)
  • dropdown.stories.tsx — 7개 Storybook 스토리 작성 (Default, WithIconAndShortcut, WithDivider, SelectDefault, SelectWithValue, SelectDisabled, SelectWithDisabledOption)
  • shared/ui/index.tsDropdownMenu, SelectDropdown 공개 API 추가

✔ Checklist

  • 코드는 로컬에서 정상적으로 빌드됩니다 (pnpm build)
  • ESLint / Prettier 통과 (pnpm lint)
  • 네이밍/레이어 컨벤션 준수 (camelCase/PascalCase, is·has 불린 접두사, alias 계층 규칙)
  • 관련 문서/주석 반영 (필요 시)
  • 주요 로직에 테스트 또는 검증 완료

Summary by CodeRabbit

  • New Features

    • Added an accessible dropdown menu (icons, shortcuts, dividers, keyboard support, click-outside/Escape dismissal).
    • Added a reusable select dropdown with disabled options, selected state indication, placeholder, and optional full-width layout.
  • Documentation

    • Added Storybook examples showcasing dropdown and select variants, states, and usage patterns.
  • Chores

    • Updated project ignore entries and consolidated dropdown exports.

@kwqnyewest kwqnyewest self-assigned this Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66341148-8d29-43f0-acf3-b2b33e33ec1c

📥 Commits

Reviewing files that changed from the base of the PR and between 8db3b39 and 7690f0f.

📒 Files selected for processing (2)
  • .gitignore
  • src/shared/ui/index.ts
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/shared/ui/index.ts

📝 Walkthrough

Walkthrough

Adds two dropdown components (DropdownMenu, SelectDropdown) with outside-click and Escape handling, Storybook stories, barrel exports, .gitignore additions for local Claude files, and removal of an unused React import in an inputBox story.

Changes

Cohort / File(s) Summary
Dropdown Components
src/shared/ui/dropdown/dropdown-menu.tsx, src/shared/ui/dropdown/select-dropdown.tsx
Added DropdownMenu and SelectDropdown components with typed props, open/close state, outside-click and Escape handlers, disabled/item behaviors, icons/shortcuts, and selection callbacks.
Storybook
src/shared/ui/dropdown/dropdown.stories.tsx
New stories demonstrating DropdownMenu variants and SelectDropdown scenarios (default, preselected, disabled, disabled option).
Barrel Exports
src/shared/ui/dropdown/index.ts, src/shared/ui/index.ts
Added re-exports for DropdownMenu, SelectDropdown, and their related types to the shared UI public surface.
Config & Minor Cleanup
.gitignore, src/shared/ui/inputBox/inputBox.stories.tsx
.gitignore: added .claude/ and package-lock.json.claude/ entries. inputBox.stories.tsx: removed unused React import.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Dropdown as DropdownComponent
  participant Parent as ParentComponent
  participant Doc as Document

  User->>Dropdown: click trigger
  Dropdown->>Dropdown: toggle isOpen (render menu)
  User->>Dropdown: click option
  Dropdown->>Parent: call onSelect/onChange(option)
  Dropdown->>Dropdown: close menu
  User->>Doc: click outside
  Doc->>Dropdown: outside-click handler → close menu
  User->>Dropdown: press Escape
  Dropdown->>Dropdown: Escape handler → close menu
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Suggested reviewers

  • kimsman06
  • jaeu5325

Poem

🐰
A tiny hop, a joyful cheer,
Menus open when choices appear,
Click a carrot, press Escape,
I code, I munch, then take a tape. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: implementing dropdown components (Dropdowns 컴포넌트 구현). It follows the repository's convention with [Feat] prefix and issue reference #54.
Description check ✅ Passed The PR description covers the core sections from the template: clear summary of the two dropdown components, detailed changes list, and completed checklist. All critical sections are present and substantive.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#54-dropdowns
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/shared/ui/index.ts (1)

3-3: Consider re-exporting types for API consistency.

The dropdown barrel (./dropdown/index.ts) exports types like DropdownMenuProps, DropdownMenuItem, SelectDropdownProps, and SelectOption. For a consistent public API, consider re-exporting these types here as well, allowing consumers to import everything from @/shared/ui.

📦 Proposed type re-exports
 export { Button } from "./button";
 export { Input } from "./input";
 export { DropdownMenu, SelectDropdown } from "./dropdown";
+export type {
+  DropdownMenuProps,
+  DropdownMenuItem,
+  SelectDropdownProps,
+  SelectOption,
+} from "./dropdown";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/index.ts` at line 3, Add the missing type re-exports to the UI
barrel so consumers can import types from the same entrypoint as components;
update the export line that currently exports DropdownMenu and SelectDropdown to
also re-export DropdownMenuProps, DropdownMenuItem, SelectDropdownProps, and
SelectOption (the exported symbols from ./dropdown) so the barrel exposes both
components and associated types for a consistent public API.
src/shared/ui/dropdown/dropdown.stories.tsx (1)

81-81: Consider consistent story typing.

The DropdownMenu stories use Story (which is StoryObj<typeof meta>), while the SelectDropdown stories use bare StoryObj. For consistency, consider defining a separate SelectStory type or using the same pattern throughout.

📝 Suggested approach

Since SelectDropdown stories have different props than the meta component (DropdownMenu), using StoryObj without type parameters is acceptable. However, you could add a comment explaining the intentional difference, or define the stories more explicitly:

// SelectDropdown stories use different component, so we use untyped StoryObj
export const SelectDefault: StoryObj = { render: () => <SelectDefaultComponent /> };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/dropdown.stories.tsx` at line 81, The SelectDropdown
stories use an untyped StoryObj (export const SelectDefault: StoryObj = {
render: () => <SelectDefaultComponent /> };) which is inconsistent with the
other stories that use a typed Story (StoryObj<typeof meta> from DropdownMenu);
make the typing explicit by either defining a SelectStory type (e.g., type
SelectStory = StoryObj<typeof SelectDefaultComponent> or similar) and using it
for SelectDefault, or add a brief comment explaining the intentional use of an
untyped StoryObj; update references to SelectDefault and SelectDefaultComponent
and the StoryObj import to reflect the chosen approach for consistency.
src/shared/ui/dropdown/dropdown-menu.tsx (2)

53-65: Consider adding ARIA attributes for better accessibility.

The dropdown trigger button is missing accessibility attributes that help screen readers understand the menu's state and relationship.

♿ Proposed accessibility improvements
       <button
         type="button"
         onClick={() => setIsOpen((prev) => !prev)}
+        aria-haspopup="menu"
+        aria-expanded={isOpen}
         className="inline-flex items-center gap-2 px-4 h-10 rounded-lg border border-(--color-gray-300,`#CCCCCC`) bg-white text-[14px] text-(--color-gray-900,`#1A1A1A`) transition-colors hover:border-(--color-primary-500,`#3385DB`) focus:outline-none focus:shadow-[0_0_0_3px_var(--color-primary-100,`#E0EDFB`)]"
       >

Also consider adding role="menu" to the dropdown panel (line 68) and role="menuitem" to each item button.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/dropdown-menu.tsx` around lines 53 - 65, The dropdown
trigger in DropdownMenu is missing ARIA attributes—update the trigger button
(where setIsOpen is used and ChevronDownIcon/trigger rendered) to include
aria-haspopup="true", aria-expanded set to the isOpen state, and aria-controls
that points to the dropdown panel's id; give the panel a stable id and set
role="menu" on the panel and role="menuitem" on each item button (or the element
rendered for each menu entry) so screen readers can understand the relationship
and state; ensure the panel id referenced by aria-controls is unique/stable and
that focus management still works with setIsOpen.

67-99: Add role attributes to the menu container and items.

For comprehensive screen reader support, the menu panel should have role="menu" and items should have role="menuitem".

♿ Proposed ARIA role additions
       {isOpen && (
-        <div className="absolute z-50 mt-1 min-w-40 rounded-lg border border-(--color-gray-200,`#E5E5E5`) bg-white shadow-[0_4px_16px_rgba(0,0,0,0.10)] py-1">
+        <div role="menu" className="absolute z-50 mt-1 min-w-40 rounded-lg border border-(--color-gray-200,`#E5E5E5`) bg-white shadow-[0_4px_16px_rgba(0,0,0,0.10)] py-1">
           {items.map((item) => (
             <React.Fragment key={item.value}>
               <button
                 type="button"
+                role="menuitem"
                 disabled={item.disabled}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/dropdown-menu.tsx` around lines 67 - 99, The dropdown
panel currently rendered when isOpen should include ARIA roles for screen
readers: add role="menu" to the outer menu container div (the one rendering
{isOpen && (<div ...>)}), and add role="menuitem" to each item button rendered
inside items.map (the button that calls handleSelect); also set
aria-disabled={item.disabled} on that button so assistive tech sees disabled
state. Ensure these attributes are applied to the same elements identified by
isOpen, the container div, the button inside items.map, and the item.disabled
check.
src/shared/ui/dropdown/select-dropdown.tsx (2)

34-50: Consider extracting shared dropdown behavior to a custom hook.

The click-outside and Escape key handling logic is duplicated between DropdownMenu and SelectDropdown. This could be extracted into a reusable hook.

♻️ Example custom hook
// useDropdownClose.ts
import { useEffect, RefObject } from "react";

export function useDropdownClose(
  containerRef: RefObject<HTMLElement>,
  isOpen: boolean,
  onClose: () => void
) {
  useEffect(() => {
    if (!isOpen) return;
    
    const handleClickOutside = (e: MouseEvent) => {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        onClose();
      }
    };
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };

    document.addEventListener("mousedown", handleClickOutside);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [isOpen, onClose, containerRef]);
}

This also adds the optimization of only registering listeners when the dropdown is open.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/select-dropdown.tsx` around lines 34 - 50, Extract the
duplicated click-outside and Escape-key logic in SelectDropdown and DropdownMenu
into a reusable hook (e.g., useDropdownClose) that accepts containerRef, isOpen,
and onClose; replace the useEffect in SelectDropdown (the effect that registers
handleClickOutside and handleKeyDown and calls setIsOpen(false)) with a call to
this hook, and update DropdownMenu to use the same hook so both components share
behavior; ensure the hook only registers listeners when isOpen is true and calls
onClose (rather than directly mutating state) and include containerRef, isOpen,
and onClose in the hook's dependency list.

73-90: Add ARIA attributes for accessibility.

Similar to DropdownMenu, the SelectDropdown trigger button should include ARIA attributes for screen reader support.

♿ Proposed accessibility improvements
       <button
         type="button"
         disabled={disabled}
         onClick={() => setIsOpen((prev) => !prev)}
+        aria-haspopup="listbox"
+        aria-expanded={isOpen}
         className={triggerClasses}
       >

Also consider adding role="listbox" to the options panel (line 93) and role="option" with aria-selected to each option button.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/select-dropdown.tsx` around lines 73 - 90, The trigger
button for SelectDropdown lacks ARIA attributes; update the button (the onClick
that uses setIsOpen and reads isOpen/selectedOption) to include
aria-haspopup="listbox", aria-expanded set from isOpen, and aria-controls
pointing to the options panel id (generate a stable id), and ensure the visible
label uses aria-label or aria-labelledby as appropriate (ties to selectedOption
or placeholder). Also add role="listbox" to the options panel element and set
each option button to role="option" with aria-selected reflecting whether the
option equals selectedOption so screen readers can announce state correctly
(adjust any affected components such as the option rendering code and
ChevronDownIcon usage accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/shared/ui/dropdown/select-dropdown.tsx`:
- Around line 106-108: Update the inconsistent fallback hex for the primary
color in the SelectDropdown component: replace the fallback "#1A5FBE" with the
actual CSS variable value "#0056b3" wherever it's used (the ternary that sets
class when isSelected and the CheckIcon class usage) so the fallback for
--color-primary-700 matches the definition in src/index.css; locate this in the
SelectDropdown component (select-dropdown.tsx) where isSelected is used and in
the CheckIcon rendering and adjust the fallback hex accordingly.

---

Nitpick comments:
In `@src/shared/ui/dropdown/dropdown-menu.tsx`:
- Around line 53-65: The dropdown trigger in DropdownMenu is missing ARIA
attributes—update the trigger button (where setIsOpen is used and
ChevronDownIcon/trigger rendered) to include aria-haspopup="true", aria-expanded
set to the isOpen state, and aria-controls that points to the dropdown panel's
id; give the panel a stable id and set role="menu" on the panel and
role="menuitem" on each item button (or the element rendered for each menu
entry) so screen readers can understand the relationship and state; ensure the
panel id referenced by aria-controls is unique/stable and that focus management
still works with setIsOpen.
- Around line 67-99: The dropdown panel currently rendered when isOpen should
include ARIA roles for screen readers: add role="menu" to the outer menu
container div (the one rendering {isOpen && (<div ...>)}), and add
role="menuitem" to each item button rendered inside items.map (the button that
calls handleSelect); also set aria-disabled={item.disabled} on that button so
assistive tech sees disabled state. Ensure these attributes are applied to the
same elements identified by isOpen, the container div, the button inside
items.map, and the item.disabled check.

In `@src/shared/ui/dropdown/dropdown.stories.tsx`:
- Line 81: The SelectDropdown stories use an untyped StoryObj (export const
SelectDefault: StoryObj = { render: () => <SelectDefaultComponent /> };) which
is inconsistent with the other stories that use a typed Story (StoryObj<typeof
meta> from DropdownMenu); make the typing explicit by either defining a
SelectStory type (e.g., type SelectStory = StoryObj<typeof
SelectDefaultComponent> or similar) and using it for SelectDefault, or add a
brief comment explaining the intentional use of an untyped StoryObj; update
references to SelectDefault and SelectDefaultComponent and the StoryObj import
to reflect the chosen approach for consistency.

In `@src/shared/ui/dropdown/select-dropdown.tsx`:
- Around line 34-50: Extract the duplicated click-outside and Escape-key logic
in SelectDropdown and DropdownMenu into a reusable hook (e.g., useDropdownClose)
that accepts containerRef, isOpen, and onClose; replace the useEffect in
SelectDropdown (the effect that registers handleClickOutside and handleKeyDown
and calls setIsOpen(false)) with a call to this hook, and update DropdownMenu to
use the same hook so both components share behavior; ensure the hook only
registers listeners when isOpen is true and calls onClose (rather than directly
mutating state) and include containerRef, isOpen, and onClose in the hook's
dependency list.
- Around line 73-90: The trigger button for SelectDropdown lacks ARIA
attributes; update the button (the onClick that uses setIsOpen and reads
isOpen/selectedOption) to include aria-haspopup="listbox", aria-expanded set
from isOpen, and aria-controls pointing to the options panel id (generate a
stable id), and ensure the visible label uses aria-label or aria-labelledby as
appropriate (ties to selectedOption or placeholder). Also add role="listbox" to
the options panel element and set each option button to role="option" with
aria-selected reflecting whether the option equals selectedOption so screen
readers can announce state correctly (adjust any affected components such as the
option rendering code and ChevronDownIcon usage accordingly).

In `@src/shared/ui/index.ts`:
- Line 3: Add the missing type re-exports to the UI barrel so consumers can
import types from the same entrypoint as components; update the export line that
currently exports DropdownMenu and SelectDropdown to also re-export
DropdownMenuProps, DropdownMenuItem, SelectDropdownProps, and SelectOption (the
exported symbols from ./dropdown) so the barrel exposes both components and
associated types for a consistent public API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0baff93a-eec6-45f0-a118-2121e4938721

📥 Commits

Reviewing files that changed from the base of the PR and between a751262 and e77bf0a.

📒 Files selected for processing (7)
  • .gitignore
  • src/shared/ui/dropdown/dropdown-menu.tsx
  • src/shared/ui/dropdown/dropdown.stories.tsx
  • src/shared/ui/dropdown/index.ts
  • src/shared/ui/dropdown/select-dropdown.tsx
  • src/shared/ui/index.ts
  • src/shared/ui/inputBox/inputBox.stories.tsx
💤 Files with no reviewable changes (1)
  • src/shared/ui/inputBox/inputBox.stories.tsx

Comment thread src/shared/ui/dropdown/select-dropdown.tsx
@kwqnyewest kwqnyewest linked an issue Mar 16, 2026 that may be closed by this pull request
7 tasks
{trigger}
<ChevronDownIcon
size={16}
className={`transition-transform duration-200 text-(--color-gray-500,#808080) ${isOpen ? "rotate-180" : ""}`}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

색상/보더 토큰 표기 방식이 기존 컴포넌트들과 다르게 들어가 있습니다!

border-(--color-gray-300,#CCCCCC), text-(--color-gray-900,#1A1A1A) 형태는 현재 프로젝트에서 사용 중인 border-[color:var(...)], text-[color:var(...)] 패턴과도 다르고 Tailwind 문법상 스타일이 적용되지 않을 수 있어 보여 확인 부탁드립니다~!

Comment thread src/shared/ui/dropdown/select-dropdown.tsx
@kwqnyewest kwqnyewest requested a review from sebeeeen March 19, 2026 01:14
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/shared/ui/dropdown/dropdown-menu.tsx (1)

29-45: Bind document listeners only when the menu is open.

On Line 39–44, global listeners are attached for the component lifetime. Gate them by isOpen to reduce unnecessary document-level handlers and avoid handling Escape while closed.

⚙️ Suggested patch
-  useEffect(() => {
+  useEffect(() => {
+    if (!isOpen) return;
+
     const handleClickOutside = (e: MouseEvent) => {
       if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
         setIsOpen(false);
       }
     };
@@
     return () => {
       document.removeEventListener("mousedown", handleClickOutside);
       document.removeEventListener("keydown", handleKeyDown);
     };
-  }, []);
+  }, [isOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/dropdown-menu.tsx` around lines 29 - 45, The
document-level handlers (handleClickOutside, handleKeyDown) are currently
registered for the component lifetime; update the useEffect surrounding
containerRef to only add those listeners when isOpen is true and remove them
when isOpen becomes false. Specifically, modify the effect that defines
handleClickOutside and handleKeyDown (the effect using containerRef, setIsOpen)
to depend on isOpen, only call document.addEventListener when isOpen is true,
and always remove the listeners in the cleanup; include isOpen in the dependency
array so Escape is not handled while the menu is closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/shared/ui/dropdown/dropdown-menu.tsx`:
- Around line 55-68: The trigger button and menu container need WAI-ARIA menu
semantics: generate a stable id (e.g., menuId via useId or useRef) and add
aria-haspopup="menu", aria-expanded={isOpen}, and aria-controls={menuId} to the
button (the element that toggles setIsOpen); mark the ChevronDownIcon as
decorative/aria-hidden; add role="menu" and id={menuId} to the div that renders
when isOpen; ensure each menu item element uses role="menuitem" and disabled
items include aria-disabled="true" (update the components that render items
accordingly).

---

Nitpick comments:
In `@src/shared/ui/dropdown/dropdown-menu.tsx`:
- Around line 29-45: The document-level handlers (handleClickOutside,
handleKeyDown) are currently registered for the component lifetime; update the
useEffect surrounding containerRef to only add those listeners when isOpen is
true and remove them when isOpen becomes false. Specifically, modify the effect
that defines handleClickOutside and handleKeyDown (the effect using
containerRef, setIsOpen) to depend on isOpen, only call
document.addEventListener when isOpen is true, and always remove the listeners
in the cleanup; include isOpen in the dependency array so Escape is not handled
while the menu is closed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ac275dd-7b7f-4475-b83a-8628ceb8ced2

📥 Commits

Reviewing files that changed from the base of the PR and between e77bf0a and f125c68.

📒 Files selected for processing (1)
  • src/shared/ui/dropdown/dropdown-menu.tsx

Comment on lines +55 to +68
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className="inline-flex items-center gap-2 px-4 h-10 rounded-lg border border-[color:var(--color-gray-300,#CCCCCC)] bg-white text-[14px] text-[color:var(--color-gray-900,#1A1A1A)] transition-colors hover:border-[color:var(--color-primary-500,#3385DB)] focus:outline-none focus:shadow-[0_0_0_3px_var(--color-primary-100,#E0EDFB)]"
>
{trigger}
<ChevronDownIcon
size={16}
className={`transition-transform duration-200 text-[color:var(--color-gray-500,#808080)] ${isOpen ? "rotate-180" : ""}`}
/>
</button>

{isOpen && (
<div className="absolute z-50 mt-1 min-w-40 rounded-lg border border-[color:var(--color-gray-200,#E5E5E5)] bg-white shadow-[0_4px_16px_rgba(0,0,0,0.10)] py-1">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/shared/ui/dropdown && cat -n dropdown-menu.tsx

Repository: ajou-industry-matching/aim-frontend

Length of output: 4565


🌐 Web query:

WAI-ARIA menu button pattern APG screen reader accessibility

💡 Result:

The WAI-ARIA Authoring Practices Guide (APG) defines the Menu Button pattern as a button (role="button") that opens a menu (role="menu") containing menu items (role="menuitem"). Key ARIA attributes include aria-haspopup="menu" or "true" on the button to indicate it opens a menu, aria-expanded="true/false" to show if the menu is open, and optionally aria-controls referencing the menu ID. The menu uses aria-labelledby pointing to the button for its accessible name. Examples include action menus using element.focus or aria-activedescendant for focus management, and navigation menus with links as menuitems. Keyboard interactions: Space/Enter/Down Arrow (optionally Up Arrow) on button opens menu and focuses first/last item; Esc closes menu; arrow keys navigate items; Enter/Space activates item. Screen reader accessibility: Screen readers recognize aria-haspopup buttons as "menu buttons" via platform APIs (e.g., NVDA/Chrome announces "actions, menu button, collapsed"; JAWS/Firefox "actions, button, menu"). APG examples have tested support with JAWS, NVDA, VoiceOver, showing name, role, expanded state, and menu navigation are conveyed. Partial support noted in some aria-activedescendant tests (95/116 for screen readers), but core pattern is robust. Test before production use, especially mobile. Live examples: https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions/, https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/. Support data: https://a11ysupport.io/tests/apgmenu-button-actions-active-descendant.

Citations:


Add ARIA menu semantics for screen-reader accessibility.

The trigger button and menu list lack core WAI-ARIA menu button pattern attributes (aria-haspopup, aria-expanded, aria-controls, role="menu", role="menuitem", aria-disabled). This prevents screen readers (NVDA, JAWS, VoiceOver) from identifying the menu structure and state.

♿ Suggested patch
-import React, { useState, useRef, useEffect } from "react";
+import React, { useState, useRef, useEffect, useId } from "react";
@@
 export const DropdownMenu: React.FC<DropdownMenuProps> = ({
@@
 }) => {
+  const menuId = useId();
   const [isOpen, setIsOpen] = useState(false);
   const containerRef = useRef<HTMLDivElement>(null);
@@
       <button
         type="button"
         onClick={() => setIsOpen((prev) => !prev)}
+        aria-haspopup="menu"
+        aria-expanded={isOpen}
+        aria-controls={menuId}
         className="inline-flex items-center gap-2 px-4 h-10 rounded-lg border border-[color:var(--color-gray-300,`#CCCCCC`)] bg-white text-[14px] text-[color:var(--color-gray-900,`#1A1A1A`)] transition-colors hover:border-[color:var(--color-primary-500,`#3385DB`)] focus:outline-none focus:shadow-[0_0_0_3px_var(--color-primary-100,`#E0EDFB`)]"
       >
@@
       {isOpen && (
-        <div className="absolute z-50 mt-1 min-w-40 rounded-lg border border-[color:var(--color-gray-200,`#E5E5E5`)] bg-white shadow-[0_4px_16px_rgba(0,0,0,0.10)] py-1">
+        <div
+          id={menuId}
+          role="menu"
+          className="absolute z-50 mt-1 min-w-40 rounded-lg border border-[color:var(--color-gray-200,`#E5E5E5`)] bg-white shadow-[0_4px_16px_rgba(0,0,0,0.10)] py-1"
+        >
           {items.map((item) => (
             <React.Fragment key={item.value}>
               <button
                 type="button"
+                role="menuitem"
+                aria-disabled={item.disabled}
                 disabled={item.disabled}
                 onClick={() => handleSelect(item)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/dropdown/dropdown-menu.tsx` around lines 55 - 68, The trigger
button and menu container need WAI-ARIA menu semantics: generate a stable id
(e.g., menuId via useId or useRef) and add aria-haspopup="menu",
aria-expanded={isOpen}, and aria-controls={menuId} to the button (the element
that toggles setIsOpen); mark the ChevronDownIcon as decorative/aria-hidden; add
role="menu" and id={menuId} to the div that renders when isOpen; ensure each
menu item element uses role="menuitem" and disabled items include
aria-disabled="true" (update the components that render items accordingly).

@kwqnyewest kwqnyewest merged commit 439dcc2 into dev Mar 19, 2026
1 check passed
@sebeeeen sebeeeen deleted the feat/#54-dropdowns branch March 23, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Dropdowns 컴포넌트 구현

2 participants