From 3e812aa5a0402a83a3006de95d38c857dba8ad8f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:03:57 +0000 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1]=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EC=9D=98=20'=EC=A0=91=EA=B7=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC'=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExportModal.tsx의 '접근 관리' 버튼에 native `disabled` 속성이 적용되어 있어 화면 판독기(Screen Reader)와 키보드 탐색(Tab) 사용자가 버튼의 존재와 그 이유(`aria-describedby`)를 인지할 수 없는 문제를 해결했습니다. Native `disabled` 대신 `aria-disabled="true"`를 사용하고, `onClick` 핸들러에서 `e.preventDefault()`를 호출하여 액션을 안전하게 차단했습니다. 또한 시각적 비활성화 상태를 유지하기 위해 CSS(opacity: 0.5, cursor: not-allowed)를 추가하고, 이에 대한 테스트 코드와 Palette 일지를 갱신했습니다. --- .jules/palette.md | 4 ++++ .../src/components/modals/ExportModal.test.tsx | 15 +++++++++++++-- frontend/src/components/modals/ExportModal.tsx | 3 ++- frontend/src/styles.css | 3 ++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index bd0f73248..231c92c67 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -57,3 +57,7 @@ ## 2026-07-30 - Add window.confirm for destructive actions **Learning:** Destructive actions like deleting groups and edge relationships previously occurred immediately without user confirmation. **Action:** Always wrap delete operations with window.confirm() dialogs and ensure corresponding tests successfully mock window.confirm. + +## 2026-08-01 - Focusable aria-disabled buttons for screen readers +**Learning:** Native `disabled` attributes on buttons remove them from the tab order, preventing screen reader users from discovering them and hearing their `aria-describedby` helper text explaining *why* they are disabled. +**Action:** When a disabled button has important explanatory context (like requiring project permissions), use `aria-disabled="true"` with custom styling (opacity and cursor) and manually prevent action (e.g., `e.preventDefault()`) instead of the native `disabled` attribute to keep it discoverable via keyboard navigation. diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx index bd89c5384..4ab429039 100644 --- a/frontend/src/components/modals/ExportModal.test.tsx +++ b/frontend/src/components/modals/ExportModal.test.tsx @@ -1,5 +1,6 @@ import '@testing-library/jest-dom/vitest'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { ExportModal } from './ExportModal'; @@ -171,13 +172,23 @@ describe('ExportModal', () => { expect(screen.getByRole('button', { name: '데이터 사전 Markdown 내보내기' })).toBeDisabled(); }); - it('exposes access-control guidance for disabled button', () => { + it('exposes access-control guidance for disabled button', async () => { + const user = userEvent.setup(); render(); expect(screen.getByText('접근 권한 관리는 프로젝트 권한 설정에서 처리합니다.')).toBeInTheDocument(); const accessManagementButton = screen.getByRole('button', { name: '접근 관리' }); - expect(accessManagementButton).toBeDisabled(); + expect(accessManagementButton).toHaveAttribute('aria-disabled', 'true'); expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint'); expect(accessManagementButton).not.toHaveAttribute('title'); + + accessManagementButton.focus(); + expect(accessManagementButton).toHaveFocus(); + + // Attempting to "click" or submit via keyboard should not trigger any action + // (though there's no specific prop to mock here, verifying it doesn't throw or navigate is sufficient, + // and we simulate the interaction to ensure the preventDefault runs) + await user.keyboard('{Enter}'); + expect(accessManagementButton).toHaveFocus(); }); }); diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx index 995393ceb..9ccda8ad9 100644 --- a/frontend/src/components/modals/ExportModal.tsx +++ b/frontend/src/components/modals/ExportModal.tsx @@ -202,7 +202,8 @@ export function ExportModal({ )}